From ded181acb8f5ef51ac128f5b699d6d46bd8d1457 Mon Sep 17 00:00:00 2001 From: Jordy de Jonghe Date: Thu, 10 Oct 2024 11:26:18 +0200 Subject: [PATCH] #349: added remote config --- lib/di/injectable.config.dart | 246 +++++++++--------- .../data/remote_config/localized_message.dart | 20 ++ .../remote_config/localized_message.g.dart | 25 ++ lib/model/webservice/todo/todo.dart | 6 +- .../base_remote_config_repository.dart | 31 +++ .../remote_config/remote_config.dart | 18 ++ .../remote_config_repository.dart | 35 +++ lib/styles/theme_durations.dart | 2 + .../localized_message_extension.dart | 20 ++ .../extension/remote_config_extension.dart | 7 + .../locale/localization_overrides_impl.dart | 21 ++ lib/viewmodel/global/global_viewmodel.dart | 9 + model_generator/config.yaml | 20 +- pubspec.lock | 24 ++ pubspec.yaml | 1 + ...g_platform_selector_screen_test.mocks.dart | 9 + .../screen/debug/debug_screen_test.mocks.dart | 9 + test/screen/home/home_screen_test.mocks.dart | 9 + .../license/license_screen_test.mocks.dart | 9 + test/util/test_util.mocks.dart | 9 + .../global/global_viewmodel_test.dart | 6 + .../global/global_viewmodel_test.mocks.dart | 34 +++ .../theme_selector_viewmodel_test.mocks.dart | 9 + .../select_language_dialog_test.mocks.dart | 9 + .../data_provider_widget_test.mocks.dart | 9 + 25 files changed, 469 insertions(+), 128 deletions(-) create mode 100644 lib/model/data/remote_config/localized_message.dart create mode 100644 lib/model/data/remote_config/localized_message.g.dart create mode 100644 lib/repository/remote_config/base_remote_config_repository.dart create mode 100644 lib/repository/remote_config/remote_config.dart create mode 100644 lib/repository/remote_config/remote_config_repository.dart create mode 100644 lib/util/extension/localized_message_extension.dart create mode 100644 lib/util/extension/remote_config_extension.dart create mode 100644 lib/util/locale/localization_overrides_impl.dart diff --git a/lib/di/injectable.config.dart b/lib/di/injectable.config.dart index 5ad78f3c..93cc267f 100644 --- a/lib/di/injectable.config.dart +++ b/lib/di/injectable.config.dart @@ -8,71 +8,77 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:dio/dio.dart' as _i41; +import 'package:dio/dio.dart' as _i44; import 'package:drift/drift.dart' as _i6; import 'package:firebase_analytics/firebase_analytics.dart' as _i8; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i9; import 'package:flutter_template/database/flutter_template_database.dart' as _i10; -import 'package:flutter_template/database/todo/todo_dao_storage.dart' as _i17; -import 'package:flutter_template/di/injectable.dart' as _i43; -import 'package:flutter_template/navigator/main_navigator.dart' as _i11; -import 'package:flutter_template/navigator/onboarding_navigator.dart' as _i30; +import 'package:flutter_template/database/todo/todo_dao_storage.dart' as _i20; +import 'package:flutter_template/di/injectable.dart' as _i46; +import 'package:flutter_template/navigator/main_navigator.dart' as _i13; +import 'package:flutter_template/navigator/onboarding_navigator.dart' as _i33; import 'package:flutter_template/repository/analytics/firebase_analytics_repository.dart' - as _i21; + as _i24; import 'package:flutter_template/repository/debug/debug_repository.dart' - as _i25; + as _i28; import 'package:flutter_template/repository/locale/locale_repository.dart' - as _i27; + as _i30; import 'package:flutter_template/repository/login/login_repository.dart' - as _i28; -import 'package:flutter_template/repository/refresh/refresh_repository.dart' as _i31; +import 'package:flutter_template/repository/refresh/refresh_repository.dart' + as _i34; +import 'package:flutter_template/repository/remote_config/remote_config.dart' + as _i16; import 'package:flutter_template/repository/secure_storage/auth/auth_storage.dart' - as _i24; + as _i27; import 'package:flutter_template/repository/secure_storage/secure_storage.dart' - as _i14; + as _i17; import 'package:flutter_template/repository/shared_prefs/local/local_storage.dart' - as _i26; -import 'package:flutter_template/repository/todo/todo_repository.dart' as _i23; + as _i29; +import 'package:flutter_template/repository/todo/todo_repository.dart' as _i26; import 'package:flutter_template/util/cache/cache_controller.dart' as _i4; import 'package:flutter_template/util/cache/cache_controlling.dart' as _i3; import 'package:flutter_template/util/interceptor/network_auth_interceptor.dart' - as _i29; + as _i32; import 'package:flutter_template/util/interceptor/network_error_interceptor.dart' - as _i12; + as _i14; import 'package:flutter_template/util/interceptor/network_log_interceptor.dart' - as _i13; + as _i15; import 'package:flutter_template/util/interceptor/network_refresh_interceptor.dart' - as _i39; + as _i42; +import 'package:flutter_template/util/locale/localization_overrides.dart' + as _i11; +import 'package:flutter_template/util/locale/localization_overrides_impl.dart' + as _i12; import 'package:flutter_template/util/snackbar/error_util.dart' as _i7; -import 'package:flutter_template/util/theme/theme_config.dart' as _i16; +import 'package:flutter_template/util/theme/theme_config.dart' as _i19; import 'package:flutter_template/viewmodel/debug/debug_platform_selector_viewmodel.dart' - as _i20; + as _i23; import 'package:flutter_template/viewmodel/debug/debug_theme_selector_viewmodel.dart' - as _i40; -import 'package:flutter_template/viewmodel/debug/debug_viewmodel.dart' as _i36; + as _i43; +import 'package:flutter_template/viewmodel/debug/debug_viewmodel.dart' as _i39; import 'package:flutter_template/viewmodel/global/global_viewmodel.dart' - as _i37; + as _i40; import 'package:flutter_template/viewmodel/license/license_viewmodel.dart' - as _i22; -import 'package:flutter_template/viewmodel/login/login_viewmodel.dart' as _i38; + as _i25; +import 'package:flutter_template/viewmodel/login/login_viewmodel.dart' as _i41; import 'package:flutter_template/viewmodel/permission/analytics_permission_viewmodel.dart' - as _i35; + as _i38; import 'package:flutter_template/viewmodel/splash/splash_viewmodel.dart' - as _i32; + as _i35; import 'package:flutter_template/viewmodel/todo/todo_add/todo_add_viewmodel.dart' - as _i33; + as _i36; import 'package:flutter_template/viewmodel/todo/todo_list/todo_list_viewmodel.dart' - as _i34; + as _i37; import 'package:flutter_template/webservice/todo/todo_dummy_service.dart' - as _i19; -import 'package:flutter_template/webservice/todo/todo_service.dart' as _i18; -import 'package:flutter_template/webservice/todo/todo_webservice.dart' as _i42; + as _i22; +import 'package:flutter_template/webservice/todo/todo_service.dart' as _i21; +import 'package:flutter_template/webservice/todo/todo_webservice.dart' as _i45; import 'package:get_it/get_it.dart' as _i1; import 'package:icapps_architecture/icapps_architecture.dart' as _i5; import 'package:injectable/injectable.dart' as _i2; -import 'package:shared_preferences/shared_preferences.dart' as _i15; +import 'package:shared_preferences/shared_preferences.dart' as _i18; const String _dummy = 'dummy'; const String _dev = 'dev'; @@ -103,118 +109,122 @@ extension GetItInjectableX on _i1.GetIt { gh.lazySingleton<_i9.FlutterSecureStorage>(() => registerModule.storage()); gh.lazySingleton<_i10.FlutterTemplateDatabase>(() => registerModule .provideFlutterTemplateDatabase(gh<_i6.DatabaseConnection>())); - gh.lazySingleton<_i11.MainNavigator>( - () => _i11.MainNavigator(gh<_i7.ErrorUtil>())); - gh.singleton<_i12.NetworkErrorInterceptor>( - () => _i12.NetworkErrorInterceptor(gh<_i5.ConnectivityHelper>())); - gh.singleton<_i13.NetworkLogInterceptor>( - () => _i13.NetworkLogInterceptor()); - gh.lazySingleton<_i14.SecureStorage>( - () => _i14.SecureStorage(gh<_i9.FlutterSecureStorage>())); - await gh.singletonAsync<_i15.SharedPreferences>( + gh.lazySingleton<_i11.LocalizationOverrides>( + () => _i12.LocalizationOverridesImpl()); + gh.lazySingleton<_i13.MainNavigator>( + () => _i13.MainNavigator(gh<_i7.ErrorUtil>())); + gh.singleton<_i14.NetworkErrorInterceptor>( + () => _i14.NetworkErrorInterceptor(gh<_i5.ConnectivityHelper>())); + gh.singleton<_i15.NetworkLogInterceptor>( + () => _i15.NetworkLogInterceptor()); + gh.lazySingleton<_i16.RemoteConfig>(() => _i16.RemoteConfig()); + gh.lazySingleton<_i17.SecureStorage>( + () => _i17.SecureStorage(gh<_i9.FlutterSecureStorage>())); + await gh.singletonAsync<_i18.SharedPreferences>( () => registerModule.prefs(), preResolve: true, ); - gh.lazySingleton<_i16.ThemeConfigUtil>(() => _i16.ThemeConfigUtil()); - gh.lazySingleton<_i17.TodoDaoStorage>( - () => _i17.TodoDaoStorage(gh<_i10.FlutterTemplateDatabase>())); - gh.singleton<_i18.TodoService>( - () => _i19.TodoDummyService(), + gh.lazySingleton<_i19.ThemeConfigUtil>(() => _i19.ThemeConfigUtil()); + gh.lazySingleton<_i20.TodoDaoStorage>( + () => _i20.TodoDaoStorage(gh<_i10.FlutterTemplateDatabase>())); + gh.singleton<_i21.TodoService>( + () => _i22.TodoDummyService(), registerFor: {_dummy}, ); - gh.factory<_i20.DebugPlatformSelectorViewModel>( - () => _i20.DebugPlatformSelectorViewModel(gh<_i11.MainNavigator>())); - gh.lazySingleton<_i21.FireBaseAnalyticsRepository>( - () => _i21.FireBaseAnalyticsRepository(gh<_i8.FirebaseAnalytics>())); - gh.factory<_i22.LicenseViewModel>( - () => _i22.LicenseViewModel(gh<_i11.MainNavigator>())); + gh.factory<_i23.DebugPlatformSelectorViewModel>( + () => _i23.DebugPlatformSelectorViewModel(gh<_i13.MainNavigator>())); + gh.lazySingleton<_i24.FireBaseAnalyticsRepository>( + () => _i24.FireBaseAnalyticsRepository(gh<_i8.FirebaseAnalytics>())); + gh.factory<_i25.LicenseViewModel>( + () => _i25.LicenseViewModel(gh<_i13.MainNavigator>())); gh.lazySingleton<_i5.SharedPreferenceStorage>( - () => registerModule.sharedPreferences(gh<_i15.SharedPreferences>())); + () => registerModule.sharedPreferences(gh<_i18.SharedPreferences>())); gh.lazySingleton<_i5.SimpleKeyValueStorage>( () => registerModule.keyValueStorage( gh<_i5.SharedPreferenceStorage>(), - gh<_i14.SecureStorage>(), + gh<_i17.SecureStorage>(), )); - gh.lazySingleton<_i23.TodoRepository>(() => _i23.TodoRepository( - gh<_i18.TodoService>(), - gh<_i17.TodoDaoStorage>(), + gh.lazySingleton<_i26.TodoRepository>(() => _i26.TodoRepository( + gh<_i21.TodoService>(), + gh<_i20.TodoDaoStorage>(), )); - gh.lazySingleton<_i24.AuthStorage>( - () => _i24.AuthStorage(gh<_i5.SimpleKeyValueStorage>())); - gh.lazySingleton<_i25.DebugRepository>( - () => _i25.DebugRepository(gh<_i5.SharedPreferenceStorage>())); - gh.lazySingleton<_i26.LocalStorage>(() => _i26.LocalStorage( - gh<_i24.AuthStorage>(), + gh.lazySingleton<_i27.AuthStorage>( + () => _i27.AuthStorage(gh<_i5.SimpleKeyValueStorage>())); + gh.lazySingleton<_i28.DebugRepository>( + () => _i28.DebugRepository(gh<_i5.SharedPreferenceStorage>())); + gh.lazySingleton<_i29.LocalStorage>(() => _i29.LocalStorage( + gh<_i27.AuthStorage>(), gh<_i5.SharedPreferenceStorage>(), )); - gh.lazySingleton<_i27.LocaleRepository>( - () => _i27.LocaleRepository(gh<_i5.SharedPreferenceStorage>())); - gh.lazySingleton<_i28.LoginRepository>( - () => _i28.LoginRepository(gh<_i24.AuthStorage>())); - gh.singleton<_i29.NetworkAuthInterceptor>( - () => _i29.NetworkAuthInterceptor(gh<_i24.AuthStorage>())); - gh.lazySingleton<_i30.OnboardingNavigator>(() => _i30.OnboardingNavigator( - gh<_i11.MainNavigator>(), - gh<_i26.LocalStorage>(), - gh<_i28.LoginRepository>(), + gh.lazySingleton<_i30.LocaleRepository>( + () => _i30.LocaleRepository(gh<_i5.SharedPreferenceStorage>())); + gh.lazySingleton<_i31.LoginRepository>( + () => _i31.LoginRepository(gh<_i27.AuthStorage>())); + gh.singleton<_i32.NetworkAuthInterceptor>( + () => _i32.NetworkAuthInterceptor(gh<_i27.AuthStorage>())); + gh.lazySingleton<_i33.OnboardingNavigator>(() => _i33.OnboardingNavigator( + gh<_i13.MainNavigator>(), + gh<_i29.LocalStorage>(), + gh<_i31.LoginRepository>(), )); - gh.lazySingleton<_i31.RefreshRepository>( - () => _i31.RefreshRepository(gh<_i24.AuthStorage>())); - gh.factory<_i32.SplashViewModel>(() => _i32.SplashViewModel( - gh<_i26.LocalStorage>(), - gh<_i30.OnboardingNavigator>(), + gh.lazySingleton<_i34.RefreshRepository>( + () => _i34.RefreshRepository(gh<_i27.AuthStorage>())); + gh.factory<_i35.SplashViewModel>(() => _i35.SplashViewModel( + gh<_i29.LocalStorage>(), + gh<_i33.OnboardingNavigator>(), )); - gh.factory<_i33.TodoAddViewModel>(() => _i33.TodoAddViewModel( - gh<_i23.TodoRepository>(), - gh<_i11.MainNavigator>(), + gh.factory<_i36.TodoAddViewModel>(() => _i36.TodoAddViewModel( + gh<_i26.TodoRepository>(), + gh<_i13.MainNavigator>(), )); - gh.factory<_i34.TodoListViewModel>(() => _i34.TodoListViewModel( - gh<_i23.TodoRepository>(), - gh<_i11.MainNavigator>(), + gh.factory<_i37.TodoListViewModel>(() => _i37.TodoListViewModel( + gh<_i26.TodoRepository>(), + gh<_i13.MainNavigator>(), )); - gh.factory<_i35.AnalyticsPermissionViewModel>( - () => _i35.AnalyticsPermissionViewModel( - gh<_i30.OnboardingNavigator>(), - gh<_i26.LocalStorage>(), + gh.factory<_i38.AnalyticsPermissionViewModel>( + () => _i38.AnalyticsPermissionViewModel( + gh<_i33.OnboardingNavigator>(), + gh<_i29.LocalStorage>(), )); - gh.factory<_i36.DebugViewModel>(() => _i36.DebugViewModel( - gh<_i25.DebugRepository>(), - gh<_i11.MainNavigator>(), + gh.factory<_i39.DebugViewModel>(() => _i39.DebugViewModel( + gh<_i28.DebugRepository>(), + gh<_i13.MainNavigator>(), gh<_i10.FlutterTemplateDatabase>(), - gh<_i26.LocalStorage>(), + gh<_i29.LocalStorage>(), )); - gh.lazySingleton<_i37.GlobalViewModel>(() => _i37.GlobalViewModel( - gh<_i27.LocaleRepository>(), - gh<_i25.DebugRepository>(), - gh<_i26.LocalStorage>(), - gh<_i16.ThemeConfigUtil>(), + gh.lazySingleton<_i40.GlobalViewModel>(() => _i40.GlobalViewModel( + gh<_i30.LocaleRepository>(), + gh<_i28.DebugRepository>(), + gh<_i29.LocalStorage>(), + gh<_i19.ThemeConfigUtil>(), + gh<_i11.LocalizationOverrides>(), )); - gh.factory<_i38.LoginViewModel>(() => _i38.LoginViewModel( - gh<_i28.LoginRepository>(), - gh<_i11.MainNavigator>(), - gh<_i30.OnboardingNavigator>(), + gh.factory<_i41.LoginViewModel>(() => _i41.LoginViewModel( + gh<_i31.LoginRepository>(), + gh<_i13.MainNavigator>(), + gh<_i33.OnboardingNavigator>(), )); - gh.singleton<_i39.NetworkRefreshInterceptor>( - () => _i39.NetworkRefreshInterceptor( - gh<_i24.AuthStorage>(), - gh<_i31.RefreshRepository>(), + gh.singleton<_i42.NetworkRefreshInterceptor>( + () => _i42.NetworkRefreshInterceptor( + gh<_i27.AuthStorage>(), + gh<_i34.RefreshRepository>(), )); gh.lazySingleton<_i5.CombiningSmartInterceptor>( () => registerModule.provideCombiningSmartInterceptor( - gh<_i13.NetworkLogInterceptor>(), - gh<_i29.NetworkAuthInterceptor>(), - gh<_i12.NetworkErrorInterceptor>(), - gh<_i39.NetworkRefreshInterceptor>(), + gh<_i15.NetworkLogInterceptor>(), + gh<_i32.NetworkAuthInterceptor>(), + gh<_i14.NetworkErrorInterceptor>(), + gh<_i42.NetworkRefreshInterceptor>(), )); - gh.factory<_i40.DebugThemeSelectorViewModel>( - () => _i40.DebugThemeSelectorViewModel( - gh<_i11.MainNavigator>(), - gh<_i37.GlobalViewModel>(), + gh.factory<_i43.DebugThemeSelectorViewModel>( + () => _i43.DebugThemeSelectorViewModel( + gh<_i13.MainNavigator>(), + gh<_i40.GlobalViewModel>(), )); - gh.lazySingleton<_i41.Dio>( + gh.lazySingleton<_i44.Dio>( () => registerModule.provideDio(gh<_i5.CombiningSmartInterceptor>())); - gh.singleton<_i18.TodoService>( - () => _i42.TodoWebService(gh<_i41.Dio>()), + gh.singleton<_i21.TodoService>( + () => _i45.TodoWebService(gh<_i44.Dio>()), registerFor: { _dev, _prod, @@ -224,4 +234,4 @@ extension GetItInjectableX on _i1.GetIt { } } -class _$RegisterModule extends _i43.RegisterModule {} +class _$RegisterModule extends _i46.RegisterModule {} diff --git a/lib/model/data/remote_config/localized_message.dart b/lib/model/data/remote_config/localized_message.dart new file mode 100644 index 00000000..86c3b647 --- /dev/null +++ b/lib/model/data/remote_config/localized_message.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +import 'package:json_annotation/json_annotation.dart'; + +part 'localized_message.g.dart'; + +@JsonSerializable(explicitToJson: true) +class LocalizedMessage { + @JsonKey(name: 'en', includeIfNull: false) + final String? en; + + const LocalizedMessage({ + this.en, + }); + + factory LocalizedMessage.fromJson(Map json) => _$LocalizedMessageFromJson(json); + + Map toJson() => _$LocalizedMessageToJson(this); + +} diff --git a/lib/model/data/remote_config/localized_message.g.dart b/lib/model/data/remote_config/localized_message.g.dart new file mode 100644 index 00000000..d6f5e2d4 --- /dev/null +++ b/lib/model/data/remote_config/localized_message.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'localized_message.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LocalizedMessage _$LocalizedMessageFromJson(Map json) => + LocalizedMessage( + en: json['en'] as String?, + ); + +Map _$LocalizedMessageToJson(LocalizedMessage instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('en', instance.en); + return val; +} diff --git a/lib/model/webservice/todo/todo.dart b/lib/model/webservice/todo/todo.dart index 339cdaa4..d6018eb7 100644 --- a/lib/model/webservice/todo/todo.dart +++ b/lib/model/webservice/todo/todo.dart @@ -1,12 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + import 'package:json_annotation/json_annotation.dart'; part 'todo.g.dart'; @JsonSerializable(explicitToJson: true) class Todo { - @JsonKey(name: 'title', required: true, includeIfNull: false) + @JsonKey(name: 'title', required: true) final String title; - @JsonKey(name: 'completed', required: true, includeIfNull: false) + @JsonKey(name: 'completed', required: true) final bool completed; @JsonKey(name: 'id', includeIfNull: false) final int? id; diff --git a/lib/repository/remote_config/base_remote_config_repository.dart b/lib/repository/remote_config/base_remote_config_repository.dart new file mode 100644 index 00000000..a3baa452 --- /dev/null +++ b/lib/repository/remote_config/base_remote_config_repository.dart @@ -0,0 +1,31 @@ +// ignore_for_file: one_member_abstracts + +import 'dart:convert'; + +abstract class BaseRemoteConfigRepo { + Future refreshRemoteConfig(); + + String? getOptionalString(String key) => getOptionalValue(key); + + int? getOptionalInt(String key) => int.tryParse(getOptionalValue(key) ?? ''); + + bool? getOptionalBool(String key) => bool.tryParse(getOptionalValue(key) ?? ''); + + double? getOptionalDouble(String key) => double.tryParse(getOptionalValue(key) ?? ''); + + String? getOptionalValue(String key); + + Map getCustomObjectMap(String key, R Function(Map json) fromJson) { + final value = getOptionalValue(key); + if (value == null) return {}; + final map = jsonDecode(value) as Map; + return map.map((key, value) => MapEntry(key, fromJson(value as Map))); + } + + List getCustomObjectList(String key, T Function(Map json) fromJson) { + final value = getOptionalValue(key); + if (value == null) return []; + final mapList = jsonDecode(value) as List; + return mapList.map((e) => fromJson(e as Map)).toList(); + } +} diff --git a/lib/repository/remote_config/remote_config.dart b/lib/repository/remote_config/remote_config.dart new file mode 100644 index 00000000..1dd6458a --- /dev/null +++ b/lib/repository/remote_config/remote_config.dart @@ -0,0 +1,18 @@ +import 'package:flutter_template/di/injectable.dart'; +import 'package:flutter_template/model/data/remote_config/localized_message.dart'; +import 'package:flutter_template/repository/remote_config/remote_config_repository.dart'; +import 'package:flutter_template/util/env/flavor_config.dart'; +import 'package:injectable/injectable.dart'; + +@lazySingleton +class RemoteConfig { + static RemoteConfigRepository? get _remoteConfig => FlavorConfig.isInTest() ? null : getIt(); + + int get minimumBuild => _remoteConfig?.getOptionalInt('minimum_build') ?? 1; + + int get latestBuild => _remoteConfig?.getOptionalInt('latest_build') ?? 1; + + int get reviewBuild => _remoteConfig?.getOptionalInt('review_build') ?? 1; + + Map get overriddenTranslations => _remoteConfig?.getCustomObjectMap('overridden_translations', LocalizedMessage.fromJson) ?? {}; +} diff --git a/lib/repository/remote_config/remote_config_repository.dart b/lib/repository/remote_config/remote_config_repository.dart new file mode 100644 index 00000000..567a932b --- /dev/null +++ b/lib/repository/remote_config/remote_config_repository.dart @@ -0,0 +1,35 @@ +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter_template/repository/remote_config/base_remote_config_repository.dart'; +import 'package:flutter_template/styles/theme_durations.dart'; +import 'package:flutter_template/util/locale/localization_overrides.dart'; +import 'package:get_it/get_it.dart'; +import 'package:icapps_architecture/icapps_architecture.dart'; + +class RemoteConfigRepository extends BaseRemoteConfigRepo { + + RemoteConfigRepository(); + + @override + Future refreshRemoteConfig() async { + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings( + RemoteConfigSettings( + fetchTimeout: ThemeDurations.remoteConfigTimeOut, + minimumFetchInterval: Duration.zero, + ), + ); + try { + await remoteConfig.fetchAndActivate(); + await GetIt.I().refreshOverrideLocalizations(); + } catch (error, trace) { + logger.error('Unable to fetch remote config. Cached or default values will be used', error: error, stackTrace: trace); + } + } + + @override + String? getOptionalValue(String key) { + final remoteConfig = FirebaseRemoteConfig.instance; + if (remoteConfig.getAll().containsKey(key)) return remoteConfig.getValue(key).asString(); + return null; + } +} diff --git a/lib/styles/theme_durations.dart b/lib/styles/theme_durations.dart index 93f791c4..c327d375 100644 --- a/lib/styles/theme_durations.dart +++ b/lib/styles/theme_durations.dart @@ -25,4 +25,6 @@ class ThemeDurations { static Duration get demoNetworkCallDuration => _isInTestResult ?? const Duration(milliseconds: 800); static Duration get snackBarDuration => _isInTestResult ?? const Duration(seconds: 3); + + static Duration get remoteConfigTimeOut => _isInTestResult ?? const Duration(seconds: 9); } diff --git a/lib/util/extension/localized_message_extension.dart b/lib/util/extension/localized_message_extension.dart new file mode 100644 index 00000000..6cdf31be --- /dev/null +++ b/lib/util/extension/localized_message_extension.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/model/data/remote_config/localized_message.dart'; +import 'package:flutter_template/util/locale/localization.dart'; + +extension LocalizedMessageExtension on LocalizedMessage { + String? localized(BuildContext context) { + final locale = Localization.of(context).locale; + if (locale == null) return en; + + final languageCode = locale.languageCode; + if (languageCode.contains('en')) return en; + return en; + } + + String? fromLocale(Locale locale) { + final languageCode = locale.languageCode; + if (languageCode.contains('en')) return en; + return en; + } +} diff --git a/lib/util/extension/remote_config_extension.dart b/lib/util/extension/remote_config_extension.dart new file mode 100644 index 00000000..9a7814ac --- /dev/null +++ b/lib/util/extension/remote_config_extension.dart @@ -0,0 +1,7 @@ + +import 'package:flutter_template/di/injectable.dart'; +import 'package:flutter_template/repository/remote_config/remote_config.dart'; + +extension RemoteConfigExtension on Object { + RemoteConfig get remoteConfig => getIt.get(); +} diff --git a/lib/util/locale/localization_overrides_impl.dart b/lib/util/locale/localization_overrides_impl.dart new file mode 100644 index 00000000..64943194 --- /dev/null +++ b/lib/util/locale/localization_overrides_impl.dart @@ -0,0 +1,21 @@ +import 'dart:ui'; + +import 'package:flutter_template/util/extension/localized_message_extension.dart'; +import 'package:flutter_template/util/extension/remote_config_extension.dart'; +import 'package:flutter_template/util/locale/localization_overrides.dart'; +import 'package:flutter_template/viewmodel/global/global_viewmodel.dart'; +import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; + +@LazySingleton(as: LocalizationOverrides) +class LocalizationOverridesImpl extends LocalizationOverrides { + @override + Future> getOverriddenLocalizations(Locale locale) async { + return remoteConfig.overriddenTranslations.map((key, value) => MapEntry(key, value.fromLocale(locale) ?? value.en ?? '')); + } + + @override + Future refreshOverrideLocalizations() async { + GetIt.I().overrideLocalizations(); + } +} diff --git a/lib/viewmodel/global/global_viewmodel.dart b/lib/viewmodel/global/global_viewmodel.dart index 0f07d027..e0663c33 100644 --- a/lib/viewmodel/global/global_viewmodel.dart +++ b/lib/viewmodel/global/global_viewmodel.dart @@ -5,6 +5,7 @@ import 'package:flutter_template/repository/shared_prefs/local/local_storage.dar import 'package:flutter_template/util/locale/localization.dart'; import 'package:flutter_template/util/locale/localization_delegate.dart'; import 'package:flutter_template/util/locale/localization_keys.dart'; +import 'package:flutter_template/util/locale/localization_overrides.dart'; import 'package:flutter_template/util/theme/theme_config.dart'; import 'package:icapps_architecture/icapps_architecture.dart'; import 'package:injectable/injectable.dart'; @@ -15,6 +16,7 @@ class GlobalViewModel with ChangeNotifierEx { final DebugRepository _debugRepo; final ThemeConfigUtil _themeConfigUtil; final LocalStorage _localStorage; + final LocalizationOverrides _localizationOverrides; var _localeDelegate = LocalizationDelegate(); var _showsTranslationKeys = false; @@ -37,6 +39,7 @@ class GlobalViewModel with ChangeNotifierEx { this._debugRepo, this._localStorage, this._themeConfigUtil, + this._localizationOverrides, ); Future init() async { @@ -50,6 +53,8 @@ class GlobalViewModel with ChangeNotifierEx { if (locale != null) { _localeDelegate = LocalizationDelegate( newLocale: locale, + showLocalizationKeys: _localeDelegate.showLocalizationKeys, + localizationOverrides: _localizationOverrides, ); } notifyListeners(); @@ -88,6 +93,7 @@ class GlobalViewModel with ChangeNotifierEx { _localeDelegate = LocalizationDelegate( newLocale: locale, showLocalizationKeys: _localeDelegate.showLocalizationKeys, + localizationOverrides: _localizationOverrides, ); notifyListeners(); } @@ -147,7 +153,10 @@ class GlobalViewModel with ChangeNotifierEx { _localeDelegate = LocalizationDelegate( newLocale: locale, showLocalizationKeys: _showsTranslationKeys, + localizationOverrides: _localizationOverrides, ); notifyListeners(); } + + void overrideLocalizations() => _initLocale(); } diff --git a/model_generator/config.yaml b/model_generator/config.yaml index 39b9e84d..ce99030c 100644 --- a/model_generator/config.yaml +++ b/model_generator/config.yaml @@ -4,11 +4,15 @@ Todo: path: webservice/todo/ properties: - id: - type: int - title: - required: true - type: String - completed: - required: true - type: bool + id: int? + title: String + completed: bool + +################################################################################## +##### REMOTE CONFIG ##### +################################################################################## + +LocalizedMessage: + path: data/remote_config + properties: + en: String? \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f7d4c586..d09ff617 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -433,6 +433,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.35" + firebase_remote_config: + dependency: "direct main" + description: + name: firebase_remote_config + sha256: "653bd94b68e2c4e89eca10db90576101f1024151f39f2d4e7c64ae6a90a5f9c5" + url: "https://pub.dev" + source: hosted + version: "4.4.7" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + sha256: "24a2c445b15de3af7e4582ebceb2aa9a1e3731d0202cb3e7a1e03012440fa07d" + url: "https://pub.dev" + source: hosted + version: "1.4.35" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: "525aa3000fd27cd023841c802010a06515e564aab2f147aa964b35f54abbf449" + url: "https://pub.dev" + source: hosted + version: "1.6.7" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f5e5b28..b7c96448 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: firebase_core: ^2.4.1 firebase_core_web: ^2.1.0 firebase_crashlytics: ^3.0.10 + firebase_remote_config: ^4.4.7 flutter: sdk: flutter flutter_cache_manager: ^3.3.0 diff --git a/test/screen/debug/debug_platform_selector_screen_test.mocks.dart b/test/screen/debug/debug_platform_selector_screen_test.mocks.dart index 5c6aebba..40edc74e 100644 --- a/test/screen/debug/debug_platform_selector_screen_test.mocks.dart +++ b/test/screen/debug/debug_platform_selector_screen_test.mocks.dart @@ -211,6 +211,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i2.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/screen/debug/debug_screen_test.mocks.dart b/test/screen/debug/debug_screen_test.mocks.dart index 34d5dc71..c6382915 100644 --- a/test/screen/debug/debug_screen_test.mocks.dart +++ b/test/screen/debug/debug_screen_test.mocks.dart @@ -385,6 +385,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i6.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/screen/home/home_screen_test.mocks.dart b/test/screen/home/home_screen_test.mocks.dart index 02ca6019..ec37842e 100644 --- a/test/screen/home/home_screen_test.mocks.dart +++ b/test/screen/home/home_screen_test.mocks.dart @@ -215,6 +215,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i2.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/screen/license/license_screen_test.mocks.dart b/test/screen/license/license_screen_test.mocks.dart index 358aff99..f08f0186 100644 --- a/test/screen/license/license_screen_test.mocks.dart +++ b/test/screen/license/license_screen_test.mocks.dart @@ -303,6 +303,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i7.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/util/test_util.mocks.dart b/test/util/test_util.mocks.dart index 27633f7e..e7ea7676 100644 --- a/test/util/test_util.mocks.dart +++ b/test/util/test_util.mocks.dart @@ -224,6 +224,15 @@ class MockGlobalViewModel extends _i2.Mock implements _i3.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/viewmodel/global/global_viewmodel_test.dart b/test/viewmodel/global/global_viewmodel_test.dart index ec50c40a..51f2bad0 100644 --- a/test/viewmodel/global/global_viewmodel_test.dart +++ b/test/viewmodel/global/global_viewmodel_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_template/repository/debug/debug_repository.dart'; import 'package:flutter_template/repository/locale/locale_repository.dart'; import 'package:flutter_template/repository/shared_prefs/local/local_storage.dart'; import 'package:flutter_template/util/locale/localization_keys.dart'; +import 'package:flutter_template/util/locale/localization_overrides.dart'; import 'package:flutter_template/util/theme/theme_config.dart'; import 'package:flutter_template/viewmodel/global/global_viewmodel.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,6 +18,7 @@ import 'global_viewmodel_test.mocks.dart'; DebugRepository, LocalStorage, ThemeConfigUtil, + LocalizationOverrides, ]) void main() { late GlobalViewModel sut; @@ -24,17 +26,21 @@ void main() { late DebugRepository debugRepo; late LocalStorage localStorage; late ThemeConfigUtil themeConfigUtil; + late LocalizationOverrides localizationOverrides; setUp(() async { localeRepo = MockLocaleRepository(); debugRepo = MockDebugRepository(); localStorage = MockLocalStorage(); themeConfigUtil = MockThemeConfigUtil(); + localizationOverrides = MockLocalizationOverrides(); + sut = GlobalViewModel( localeRepo, debugRepo, localStorage, themeConfigUtil, + localizationOverrides, ); }); tearDown(() { diff --git a/test/viewmodel/global/global_viewmodel_test.mocks.dart b/test/viewmodel/global/global_viewmodel_test.mocks.dart index 77df2366..0555f4ae 100644 --- a/test/viewmodel/global/global_viewmodel_test.mocks.dart +++ b/test/viewmodel/global/global_viewmodel_test.mocks.dart @@ -12,6 +12,8 @@ import 'package:flutter_template/repository/locale/locale_repository.dart' as _i2; import 'package:flutter_template/repository/shared_prefs/local/local_storage.dart' as _i6; +import 'package:flutter_template/util/locale/localization_overrides.dart' + as _i9; import 'package:flutter_template/util/theme/theme_config.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; @@ -152,3 +154,35 @@ class MockThemeConfigUtil extends _i1.Mock implements _i8.ThemeConfigUtil { returnValueForMissingStub: null, ); } + +/// A class which mocks [LocalizationOverrides]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalizationOverrides extends _i1.Mock + implements _i9.LocalizationOverrides { + MockLocalizationOverrides() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future refreshOverrideLocalizations() => (super.noSuchMethod( + Invocation.method( + #refreshOverrideLocalizations, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future> getOverriddenLocalizations( + _i4.Locale? locale) => + (super.noSuchMethod( + Invocation.method( + #getOverriddenLocalizations, + [locale], + ), + returnValue: + _i3.Future>.value({}), + ) as _i3.Future>); +} diff --git a/test/viewmodel/theme_selector/theme_selector_viewmodel_test.mocks.dart b/test/viewmodel/theme_selector/theme_selector_viewmodel_test.mocks.dart index eff9c3bd..33da3243 100644 --- a/test/viewmodel/theme_selector/theme_selector_viewmodel_test.mocks.dart +++ b/test/viewmodel/theme_selector/theme_selector_viewmodel_test.mocks.dart @@ -223,6 +223,15 @@ class MockGlobalViewModel extends _i2.Mock implements _i3.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/widget/debug/select_language_dialog_test.mocks.dart b/test/widget/debug/select_language_dialog_test.mocks.dart index 7be414d9..b4c8b406 100644 --- a/test/widget/debug/select_language_dialog_test.mocks.dart +++ b/test/widget/debug/select_language_dialog_test.mocks.dart @@ -209,6 +209,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i2.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method( diff --git a/test/widget/provider/data_provider_widget_test.mocks.dart b/test/widget/provider/data_provider_widget_test.mocks.dart index e8ca8b2e..5d2f3a24 100644 --- a/test/widget/provider/data_provider_widget_test.mocks.dart +++ b/test/widget/provider/data_provider_widget_test.mocks.dart @@ -209,6 +209,15 @@ class MockGlobalViewModel extends _i1.Mock implements _i2.GlobalViewModel { returnValueForMissingStub: null, ); + @override + void overrideLocalizations() => super.noSuchMethod( + Invocation.method( + #overrideLocalizations, + [], + ), + returnValueForMissingStub: null, + ); + @override void dispose() => super.noSuchMethod( Invocation.method(