diff --git a/.gitignore b/.gitignore index 6272ab93..73dcc4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -325,3 +325,7 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + +# Firebase +*google-services.json +*GoogleService-Info.plist diff --git a/src/app/ApplicationTemplate.Business/ForcedUpdates/UpdateRequiredService.cs b/src/app/ApplicationTemplate.Business/ForcedUpdates/UpdateRequiredService.cs index 5882edd6..f2c23db2 100644 --- a/src/app/ApplicationTemplate.Business/ForcedUpdates/UpdateRequiredService.cs +++ b/src/app/ApplicationTemplate.Business/ForcedUpdates/UpdateRequiredService.cs @@ -18,7 +18,13 @@ public sealed class UpdateRequiredService : IUpdateRequiredService, IDisposable public UpdateRequiredService(IMinimumVersionReposiory minimumVersionReposiory) { _subscription = minimumVersionReposiory.MinimumVersionObservable - .Subscribe(_ => UpdateRequired?.Invoke(this, EventArgs.Empty)); + .Subscribe(version => + { + if (version > new Version("1.1.0")) + { + UpdateRequired?.Invoke(this, EventArgs.Empty); + } + }); } /// diff --git a/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj b/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj index 6678ad4b..cb9d5bdf 100644 --- a/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj +++ b/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj @@ -15,6 +15,9 @@ True partial + + + @@ -100,8 +103,11 @@ - + + + + @@ -149,10 +155,19 @@ $(MtouchExtraArgs) --registrar:static $(MtouchExtraArgs) --marshal-objectivec-exceptions:disable + + $(MtouchExtraArgs) --gcc_flags -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos + $(MtouchExtraArgs) --gcc_flags -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator/ iOS\Entitlements.plist true + + + + GoogleService-Info.plist + + @@ -201,6 +216,7 @@ + @@ -268,4 +284,8 @@ + + + + \ No newline at end of file diff --git a/src/app/ApplicationTemplate.Mobile/LinkWithSwiftSystemLibrariesWorkaround.targets b/src/app/ApplicationTemplate.Mobile/LinkWithSwiftSystemLibrariesWorkaround.targets new file mode 100644 index 00000000..9497a7ae --- /dev/null +++ b/src/app/ApplicationTemplate.Mobile/LinkWithSwiftSystemLibrariesWorkaround.targets @@ -0,0 +1,18 @@ + + + + + + <_SwiftPlatform Condition="$(RuntimeIdentifier.StartsWith('iossimulator-'))">iphonesimulator + <_SwiftPlatform Condition="$(RuntimeIdentifier.StartsWith('ios-'))">iphoneos + + + <_CustomLinkFlags Include="-L" /> + <_CustomLinkFlags Include="/usr/lib/swift" /> + <_CustomLinkFlags Include="-L" /> + <_CustomLinkFlags Include="$(_SdkDevPath)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(_SwiftPlatform)" /> + <_CustomLinkFlags Include="-Wl,-rpath" /> + <_CustomLinkFlags Include="-Wl,/usr/lib/swift" /> + + + \ No newline at end of file diff --git a/src/app/ApplicationTemplate.Presentation/Configuration/ApiConfiguration.cs b/src/app/ApplicationTemplate.Presentation/Configuration/ApiConfiguration.cs index 1bab32ae..0f622457 100644 --- a/src/app/ApplicationTemplate.Presentation/Configuration/ApiConfiguration.cs +++ b/src/app/ApplicationTemplate.Presentation/Configuration/ApiConfiguration.cs @@ -42,8 +42,6 @@ public static IServiceCollection AddApi(this IServiceCollection services, IConfi .AddAuthentication() .AddPosts(configuration) .AddUserProfile() - .AddMinimumVersion() - .AddKillSwitch() .AddDadJokes(configuration); return services; @@ -55,18 +53,6 @@ private static IServiceCollection AddUserProfile(this IServiceCollection service return services.AddSingleton(); } - private static IServiceCollection AddMinimumVersion(this IServiceCollection services) - { - // This one doesn't have an actual remote API yet. It's always a mock implementation. - return services.AddSingleton(); - } - - private static IServiceCollection AddKillSwitch(this IServiceCollection services) - { - // This one doesn't have an actual remote API yet. It's always a mock implementation. - return services.AddSingleton(); - } - private static IServiceCollection AddAuthentication(this IServiceCollection services) { // This one doesn't have an actual remote API yet. It's always a mock implementation. diff --git a/src/app/ApplicationTemplate.Shared.Views/ApplicationTemplate.Shared.Views.projitems b/src/app/ApplicationTemplate.Shared.Views/ApplicationTemplate.Shared.Views.projitems index 059e75fd..9da107ed 100644 --- a/src/app/ApplicationTemplate.Shared.Views/ApplicationTemplate.Shared.Views.projitems +++ b/src/app/ApplicationTemplate.Shared.Views/ApplicationTemplate.Shared.Views.projitems @@ -118,6 +118,8 @@ + + Shell.xaml diff --git a/src/app/ApplicationTemplate.Shared.Views/Configuration/ViewServicesConfiguration.cs b/src/app/ApplicationTemplate.Shared.Views/Configuration/ViewServicesConfiguration.cs index b6d91d4d..ff128d4c 100644 --- a/src/app/ApplicationTemplate.Shared.Views/Configuration/ViewServicesConfiguration.cs +++ b/src/app/ApplicationTemplate.Shared.Views/Configuration/ViewServicesConfiguration.cs @@ -1,4 +1,5 @@ using System.Reactive.Concurrency; +using ApplicationTemplate.DataAccess; using Chinook.DynamicMvvm; using MessageDialogService; using Microsoft.Extensions.DependencyInjection; @@ -34,6 +35,14 @@ public static IServiceCollection AddViewServices(this IServiceCollection service .AddSingleton() .AddSingleton() .AddSingleton() +#if __ANDROID__ || __IOS__ + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()) +#else + .AddSingleton() + .AddSingleton() +#endif .AddMessageDialog(); } diff --git a/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Android.cs b/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Android.cs new file mode 100644 index 00000000..3f02c45c --- /dev/null +++ b/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Android.cs @@ -0,0 +1,114 @@ +#if __ANDROID__ +using System; +using System.Reactive.Subjects; +using ApplicationTemplate.DataAccess; +using Firebase.RemoteConfig; + +namespace ApplicationTemplate.Views; + +/// +/// RemoteConfigRepository is a repository for Firebase Remote Config. +/// +public sealed class RemoteConfigRepository : IKillSwitchRepository, IMinimumVersionReposiory, IDisposable +{ + private Subject _killSwitchSubject = new Subject(); + private Subject _versionSubject = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + public RemoteConfigRepository() + { + FirebaseRemoteConfig mFirebaseRemoteConfig = FirebaseRemoteConfig.Instance; + FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder() + .SetMinimumFetchIntervalInSeconds(3600) + .Build(); + mFirebaseRemoteConfig.SetConfigSettingsAsync(configSettings); + + FetchRemoteConfig(); + ListenForRealTimeChanges(); + } + + private void FetchRemoteConfig() + { + FirebaseRemoteConfig mFirebaseRemoteConfig = FirebaseRemoteConfig.Instance; + mFirebaseRemoteConfig.FetchAndActivate() + .AddOnCompleteListener(new FetchCompleteListener(this)); + } + + private void ListenForRealTimeChanges() + { + FirebaseRemoteConfig mFirebaseRemoteConfig = FirebaseRemoteConfig.Instance; + + mFirebaseRemoteConfig.AddOnConfigUpdateListener(new ConfigUpdateListener(this)); + } + + /// + public IObservable MinimumVersionObservable => _versionSubject; + + /// + public void CheckMinimumVersion() + { + throw new NotImplementedException(); + } + + /// + public IObservable ObserveKillSwitchActivation() + { + return _killSwitchSubject; + } + + /// + public void Dispose() + { + _killSwitchSubject.Dispose(); + _versionSubject.Dispose(); + } + + private sealed class FetchCompleteListener : Java.Lang.Object, Android.Gms.Tasks.IOnCompleteListener + { + private readonly RemoteConfigRepository _repository; + + public FetchCompleteListener(RemoteConfigRepository repository) + { + _repository = repository; + } + + public void OnComplete(Android.Gms.Tasks.Task task) + { + if (task.IsSuccessful) + { + _repository._killSwitchSubject.OnNext(FirebaseRemoteConfig.Instance.GetBoolean("kill_switch")); + _repository._versionSubject.OnNext(new Version(FirebaseRemoteConfig.Instance.GetString("minimum_version"))); + } + } + } + + private sealed class ConfigUpdateListener : Java.Lang.Object, IConfigUpdateListener + { + private readonly RemoteConfigRepository _repository; + + public ConfigUpdateListener(RemoteConfigRepository repository) + { + _repository = repository; + } + + public void OnError(FirebaseRemoteConfigException p0) + { + throw new NotImplementedException(); + } + + public void OnUpdate(ConfigUpdate p0) + { + var instance = FirebaseRemoteConfig.Instance; + + instance.FetchAndActivate(); + var isKillSwitchActivated = FirebaseRemoteConfig.Instance.GetBoolean("kill_switch"); + var minimumVersion = new Version(FirebaseRemoteConfig.Instance.GetString("minimum_version")); + + _repository._killSwitchSubject.OnNext(isKillSwitchActivated); + _repository._versionSubject.OnNext(minimumVersion); + } + } +} +#endif diff --git a/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Ios.cs b/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Ios.cs new file mode 100644 index 00000000..4374777d --- /dev/null +++ b/src/app/ApplicationTemplate.Shared.Views/RemoteConfigRepository.Ios.cs @@ -0,0 +1,96 @@ +#if __IOS__ +using System; +using System.Reactive.Subjects; +using ApplicationTemplate.DataAccess; +using Firebase.RemoteConfig; +using Foundation; + +namespace ApplicationTemplate.Views; + +/// +/// Implementation of the killswitch repository and the minimum version with Firebase Remote Config for iOS. +/// +public sealed class RemoteConfigRepository : IKillSwitchRepository, IMinimumVersionReposiory, IDisposable +{ + private BehaviorSubject _killSwitchSubject = new BehaviorSubject(default); + private BehaviorSubject _versionSubject = new BehaviorSubject(default); + + /// + /// Initializes a new instance of the class. + /// + public RemoteConfigRepository() + { + Firebase.Core.App.Configure(); + // Enabling developer mode, allows for frequent refreshes of the cache + RemoteConfig.SharedInstance.ConfigSettings = new RemoteConfigSettings(); + + object[] values = { false, "1.0.0" }; + object[] keys = { "kill_switch", "minimum_version" }; + var defaultValues = NSDictionary.FromObjectsAndKeys(values, keys, keys.Length); + RemoteConfig.SharedInstance.SetDefaults(defaultValues); + + // CacheExpirationSeconds is set to CacheExpiration here, indicating that any previously + // fetched and cached config would be considered expired because it would have been fetched + // more than CacheExpiration seconds ago. Thus the next fetch would go to the server unless + // throttling is in progress. The default expiration duration is 43200 (12 hours). + RemoteConfig.SharedInstance.Fetch(10, (status, error) => + { + switch (status) + { + case RemoteConfigFetchStatus.Success: + Console.WriteLine("Config Fetched!"); + + // Call this method to make fetched parameter values available to your app + RemoteConfig.SharedInstance.Activate(); + + _killSwitchSubject.OnNext(RemoteConfig.SharedInstance["kill_switch"].BoolValue); + _versionSubject.OnNext(new Version(RemoteConfig.SharedInstance["minimum_version"].StringValue)); + break; + + case RemoteConfigFetchStatus.Throttled: + case RemoteConfigFetchStatus.NoFetchYet: + case RemoteConfigFetchStatus.Failure: + Console.WriteLine("Config not fetched..."); + break; + } + }); + + RemoteConfig.SharedInstance.AddObserver(new NSString("kill_switch"), NSKeyValueObservingOptions.New, (nSObservedChange) => + { + if (nSObservedChange.NewValue is NSNumber nSNumber) + { + _killSwitchSubject.OnNext(nSNumber.BoolValue); + } + }); + RemoteConfig.SharedInstance.AddObserver(new NSString("minimum_version"), NSKeyValueObservingOptions.New, (nSObservedChange) => + { + if (nSObservedChange.NewValue is NSString nSString) + { + _versionSubject.OnNext(new Version(nSString)); + } + }); + } + + /// + public IObservable MinimumVersionObservable => _versionSubject; + + /// + public void CheckMinimumVersion() + { + throw new NotImplementedException(); + } + + /// + public IObservable ObserveKillSwitchActivation() + { + return _killSwitchSubject; + } + + /// + public void Dispose() + { + _killSwitchSubject.Dispose(); + _versionSubject.Dispose(); + } +} +#endif