diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 102c04e6a..88bc70bf7 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -10,19 +10,19 @@ PODS: - Flutter - file_saver (0.0.1): - Flutter - - Firebase/CoreOnly (10.22.0): - - FirebaseCore (= 10.22.0) - - Firebase/Messaging (10.22.0): + - Firebase/CoreOnly (10.24.0): + - FirebaseCore (= 10.24.0) + - Firebase/Messaging (10.24.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.22.0) - - firebase_core (2.29.0): - - Firebase/CoreOnly (= 10.22.0) + - FirebaseMessaging (~> 10.24.0) + - firebase_core (2.30.0): + - Firebase/CoreOnly (= 10.24.0) - Flutter - - firebase_messaging (14.7.19): - - Firebase/Messaging (= 10.22.0) + - firebase_messaging (14.8.1): + - Firebase/Messaging (= 10.24.0) - firebase_core - Flutter - - FirebaseCore (10.22.0): + - FirebaseCore (10.24.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) @@ -33,7 +33,7 @@ PODS: - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.22.0): + - FirebaseMessaging (10.24.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.3) @@ -175,7 +175,7 @@ PODS: - SDWebImage (5.19.1): - SDWebImage/Core (= 5.19.1) - SDWebImage/Core (5.19.1) - - SDWebImageWebPCoder (0.14.5): + - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - Sentry/HybridSDK (8.21.0): @@ -193,14 +193,14 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS - - sqlite3 (3.45.1): - - sqlite3/common (= 3.45.1) - - sqlite3/common (3.45.1) - - sqlite3/fts5 (3.45.1): + - "sqlite3 (3.45.3+1)": + - "sqlite3/common (= 3.45.3+1)" + - "sqlite3/common (3.45.3+1)" + - "sqlite3/fts5 (3.45.3+1)": - sqlite3/common - - sqlite3/perf-threadsafe (3.45.1): + - "sqlite3/perf-threadsafe (3.45.3+1)": - sqlite3/common - - sqlite3/rtree (3.45.1): + - "sqlite3/rtree (3.45.3+1)": - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -404,13 +404,13 @@ SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 - Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71 - firebase_core: aaadbddb3cb2ee3792b9804f9dbb63e5f6f7b55c - firebase_messaging: e65050bf9b187511d80ea3a4de7cf5573d2c7543 - FirebaseCore: 0326ec9b05fbed8f8716cddbf0e36894a13837f7 + Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 + firebase_core: 66b99b4fb4e5d7cc4e88d4c195fe986681f3466a + firebase_messaging: 0eb0425d28b4f4af147cdd4adcaf7c0100df28ed + FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af FirebaseInstallations: 8f581fca6478a50705d2bd2abd66d306e0f5736e - FirebaseMessaging: 9f71037fd9db3376a4caa54e5a3949d1027b4b6e + FirebaseMessaging: 4d52717dd820707cc4eadec5eb981b4832ec8d5d fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b @@ -452,14 +452,14 @@ SPEC CHECKSUMS: receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb - SDWebImageWebPCoder: c94f09adbca681822edad9e532ac752db713eabf + SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: ebc12276bd17613a114ab359074096b6b3725203 sentry_flutter: 88ebea3f595b0bc16acc5bedacafe6d60c12dcd5 SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 + sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index 1eb59f61a..c2d08d903 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -39,13 +39,6 @@ const dragSensitivity = 8; const supportEmail = 'support@ente.io'; -// Default values for various feature flags -class FFDefault { - static const bool enableStripe = true; - static const bool disableCFWorker = false; - static const bool enablePasskey = false; -} - // this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. const multipartPartSize = 20 * 1024 * 1024; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 97338f55f..04d6c1b1b 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -21,12 +21,12 @@ import 'package:photos/core/network/network.dart'; import 'package:photos/db/upload_locks_db.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/l10n/l10n.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/collections_service.dart'; import "package:photos/services/entity_service.dart"; import 'package:photos/services/favorites_service.dart'; -import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/services/home_widget_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import 'package:photos/services/local_sync_service.dart'; @@ -178,6 +178,7 @@ Future _init(bool isBackground, {String via = ''}) async { _isProcessRunning = true; _logger.info("Initializing... inBG =$isBackground via: $via"); final SharedPreferences preferences = await SharedPreferences.getInstance(); + await _logFGHeartBeatInfo(); unawaited(_scheduleHeartBeat(preferences, isBackground)); AppLifecycleService.instance.init(preferences); @@ -191,6 +192,7 @@ Future _init(bool isBackground, {String via = ''}) async { CryptoUtil.init(); await Configuration.instance.init(); await NetworkClient.instance.init(); + ServiceLocator.instance.init(preferences, NetworkClient.instance.enteDio); await UserService.instance.init(); await EntityService.instance.init(); LocationService.instance.init(preferences); @@ -224,7 +226,7 @@ Future _init(bool isBackground, {String via = ''}) async { ); }); } - unawaited(FeatureFlagService.instance.init()); + unawaited(SemanticSearchService.instance.init()); MachineLearningController.instance.init(); // Can not including existing tf/ml binaries as they are not being built diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 75a40c99b..2aa5a4558 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -9,7 +9,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/file_magic.dart"; -import 'package:photos/services/feature_flag_service.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/utils/date_time_util.dart'; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_uploader_util.dart'; @@ -244,8 +244,7 @@ class EnteFile { String get downloadUrl { final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || - FeatureFlagService.instance.disableCFWorker()) { + if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { return endpoint + "/files/download/" + uploadedFileID.toString(); } else { return "https://files.ente.io/?fileID=" + uploadedFileID.toString(); @@ -258,8 +257,7 @@ class EnteFile { String get thumbnailUrl { final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || - FeatureFlagService.instance.disableCFWorker()) { + if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { return endpoint + "/files/preview/" + uploadedFileID.toString(); } else { return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString(); diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart new file mode 100644 index 000000000..0fec75b46 --- /dev/null +++ b/mobile/lib/service_locator.dart @@ -0,0 +1,28 @@ +import "package:dio/dio.dart"; +import "package:ente_feature_flag/ente_feature_flag.dart"; +import "package:shared_preferences/shared_preferences.dart"; + +class ServiceLocator { + late final SharedPreferences prefs; + late final Dio enteDio; + + // instance + ServiceLocator._privateConstructor(); + + static final ServiceLocator instance = ServiceLocator._privateConstructor(); + + init(SharedPreferences prefs, Dio enteDio) { + this.prefs = prefs; + this.enteDio = enteDio; + } +} + +FlagService? _flagService; + +FlagService get flagService { + _flagService ??= FlagService( + ServiceLocator.instance.prefs, + ServiceLocator.instance.enteDio, + ); + return _flagService!; +} diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 0716ea18e..0981eb767 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -30,9 +30,9 @@ import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/files_split.dart"; import "package:photos/models/metadata/collection_magic.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/favorites_service.dart"; -import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/file_magic_service.dart'; import 'package:photos/services/local_sync_service.dart'; import 'package:photos/services/remote_sync_service.dart'; @@ -1179,7 +1179,7 @@ class CollectionsService { await _addToCollection(dstCollectionID, splitResult.ownedByCurrentUser); } if (splitResult.ownedByOtherUsers.isNotEmpty) { - if (!FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + if (!flagService.internalUser) { throw ArgumentError('Cannot add files owned by other users'); } late final List filesToCopy; diff --git a/mobile/lib/services/feature_flag_service.dart b/mobile/lib/services/feature_flag_service.dart deleted file mode 100644 index 2891b03f6..000000000 --- a/mobile/lib/services/feature_flag_service.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -import 'package:photos/core/configuration.dart'; -import 'package:photos/core/constants.dart'; -import 'package:photos/core/network/network.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class FeatureFlagService { - FeatureFlagService._privateConstructor(); - - static final FeatureFlagService instance = - FeatureFlagService._privateConstructor(); - static const _featureFlagsKey = "feature_flags_key"; - static final _internalUserIDs = const String.fromEnvironment( - "internal_user_ids", - defaultValue: "1,2,3,4,191,125,1580559962388044,1580559962392434,10000025", - ).split(",").map((element) { - return int.parse(element); - }).toSet(); - - final _logger = Logger("FeatureFlagService"); - FeatureFlags? _featureFlags; - late SharedPreferences _prefs; - - Future init() async { - _prefs = await SharedPreferences.getInstance(); - // Fetch feature flags from network in async manner. - // Intention of delay is to give more CPU cycles to other tasks - Future.delayed( - const Duration(seconds: 5), - () { - fetchFeatureFlags(); - }, - ); - } - - FeatureFlags _getFeatureFlags() { - _featureFlags ??= - FeatureFlags.fromJson(_prefs.getString(_featureFlagsKey)!); - // if nothing is cached, use defaults as temporary fallback - if (_featureFlags == null) { - return FeatureFlags.defaultFlags; - } - return _featureFlags!; - } - - bool disableCFWorker() { - try { - return _getFeatureFlags().disableCFWorker; - } catch (e) { - _logger.severe(e); - return FFDefault.disableCFWorker; - } - } - - bool enableStripe() { - if (Platform.isIOS) { - return false; - } - try { - return _getFeatureFlags().enableStripe; - } catch (e) { - _logger.severe(e); - return FFDefault.enableStripe; - } - } - - bool enablePasskey() { - try { - if (isInternalUserOrDebugBuild()) { - return true; - } - return _getFeatureFlags().enablePasskey; - } catch (e) { - _logger.info('error in enablePasskey check', e); - return FFDefault.enablePasskey; - } - } - - bool isInternalUserOrDebugBuild() { - final String? email = Configuration.instance.getEmail(); - final userID = Configuration.instance.getUserID(); - return (email != null && email.endsWith("@ente.io")) || - _internalUserIDs.contains(userID) || - kDebugMode; - } - - Future fetchFeatureFlags() async { - try { - final response = await NetworkClient.instance - .getDio() - .get("https://static.ente.io/feature_flags.json"); - final flagsResponse = FeatureFlags.fromMap(response.data); - await _prefs.setString(_featureFlagsKey, flagsResponse.toJson()); - _featureFlags = flagsResponse; - } catch (e) { - _logger.severe("Failed to sync feature flags ", e); - } - } -} - -class FeatureFlags { - static FeatureFlags defaultFlags = FeatureFlags( - disableCFWorker: FFDefault.disableCFWorker, - enableStripe: FFDefault.enableStripe, - enablePasskey: FFDefault.enablePasskey, - ); - - final bool disableCFWorker; - final bool enableStripe; - final bool enablePasskey; - - FeatureFlags({ - required this.disableCFWorker, - required this.enableStripe, - required this.enablePasskey, - }); - - Map toMap() { - return { - "disableCFWorker": disableCFWorker, - "enableStripe": enableStripe, - "enablePasskey": enablePasskey, - }; - } - - String toJson() => json.encode(toMap()); - - factory FeatureFlags.fromJson(String source) => - FeatureFlags.fromMap(json.decode(source)); - - factory FeatureFlags.fromMap(Map json) { - return FeatureFlags( - disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker, - enableStripe: json["enableStripe"] ?? FFDefault.enableStripe, - enablePasskey: json["enablePasskey"] ?? FFDefault.enablePasskey, - ); - } -} diff --git a/mobile/lib/services/remote_sync_service.dart b/mobile/lib/services/remote_sync_service.dart index 4c5222758..eab8478a6 100644 --- a/mobile/lib/services/remote_sync_service.dart +++ b/mobile/lib/services/remote_sync_service.dart @@ -23,9 +23,9 @@ import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/upload_strategy.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import "package:photos/services/notification_service.dart"; @@ -185,7 +185,7 @@ class RemoteSyncService { rethrow; } else { _logger.severe("Error executing remote sync ", e, s); - if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + if (flagService.internalUser) { rethrow; } } diff --git a/mobile/lib/ui/payment/subscription.dart b/mobile/lib/ui/payment/subscription.dart index 0327c3ab5..c30a1c67d 100644 --- a/mobile/lib/ui/payment/subscription.dart +++ b/mobile/lib/ui/payment/subscription.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/services/feature_flag_service.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/services/update_service.dart'; import "package:photos/ui/payment/store_subscription_page.dart"; import 'package:photos/ui/payment/stripe_subscription_page.dart'; @@ -9,8 +9,7 @@ StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) { if (UpdateService.instance.isIndependentFlavor()) { return StripeSubscriptionPage(isOnboarding: isOnBoarding); } - if (FeatureFlagService.instance.enableStripe() && - _isUserCreatedPostStripeSupport()) { + if (flagService.enableStripe && _isUserCreatedPostStripeSupport()) { return StripeSubscriptionPage(isOnboarding: isOnBoarding); } else { return StoreSubscriptionPage(isOnboarding: isOnBoarding); diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index 0ad5bce31..3306ea36f 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -5,7 +5,7 @@ import "package:intl/intl.dart"; import "package:photos/core/event_bus.dart"; import 'package:photos/events/embedding_updated_event.dart'; import "package:photos/generated/l10n.dart"; -import "package:photos/services/feature_flag_service.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import "package:photos/theme/ente_theme.dart"; @@ -151,7 +151,7 @@ class _MachineLearningSettingsPageState const SizedBox( height: 12, ), - FeatureFlagService.instance.isInternalUserOrDebugBuild() + flagService.internalUser ? MenuItemWidget( leadingIcon: Icons.delete_sweep_outlined, captionedTextWidget: CaptionedTextWidget( diff --git a/mobile/lib/ui/settings/security_section_widget.dart b/mobile/lib/ui/settings/security_section_widget.dart index dce7e97ec..eb93d85f6 100644 --- a/mobile/lib/ui/settings/security_section_widget.dart +++ b/mobile/lib/ui/settings/security_section_widget.dart @@ -10,7 +10,7 @@ import 'package:photos/events/two_factor_status_change_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/user_details.dart"; -import "package:photos/services/feature_flag_service.dart"; +import 'package:photos/service_locator.dart'; import 'package:photos/services/local_authentication_service.dart'; import "package:photos/services/passkey_service.dart"; import 'package:photos/services/user_service.dart'; @@ -70,8 +70,6 @@ class _SecuritySectionWidgetState extends State { final Completer completer = Completer(); final List children = []; if (_config.hasConfiguredAccount()) { - final bool isInternalUser = - FeatureFlagService.instance.isInternalUserOrDebugBuild(); children.addAll( [ sectionOptionSpacing, @@ -103,8 +101,8 @@ class _SecuritySectionWidgetState extends State { }, ), ), - if (isInternalUser) sectionOptionSpacing, - if (isInternalUser) + if (flagService.passKeyEnabled) sectionOptionSpacing, + if (flagService.passKeyEnabled) MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: context.l10n.passkey, diff --git a/mobile/lib/ui/settings_page.dart b/mobile/lib/ui/settings_page.dart index 51db27595..d5ba1254f 100644 --- a/mobile/lib/ui/settings_page.dart +++ b/mobile/lib/ui/settings_page.dart @@ -7,7 +7,7 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/opened_settings_event.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/feature_flag_service.dart'; +import "package:photos/service_locator.dart"; import "package:photos/services/storage_bonus_service.dart"; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; @@ -140,8 +140,7 @@ class SettingsPage extends StatelessWidget { const AboutSectionWidget(), ]); - if (hasLoggedIn && - FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + if (hasLoggedIn && flagService.internalUser) { contents.addAll([sectionSpacing, const DebugSectionWidget()]); } contents.add(const AppVersionWidget()); diff --git a/mobile/lib/ui/tools/debug/app_storage_viewer.dart b/mobile/lib/ui/tools/debug/app_storage_viewer.dart index 055457e08..50ec16c25 100644 --- a/mobile/lib/ui/tools/debug/app_storage_viewer.dart +++ b/mobile/lib/ui/tools/debug/app_storage_viewer.dart @@ -7,7 +7,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:photos/core/cache/video_cache_manager.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/feature_flag_service.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; @@ -34,7 +34,7 @@ class _AppStorageViewerState extends State { @override void initState() { - internalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild(); + internalUser = flagService.internalUser; addPath(); super.initState(); } diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index 46ab4cb1e..e2e29e021 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -14,8 +14,8 @@ import 'package:photos/models/files_split.dart'; import 'package:photos/models/gallery_type.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/hidden_service.dart'; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; @@ -98,7 +98,7 @@ class _FileSelectionActionsWidgetState @override Widget build(BuildContext context) { - _isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild(); + _isInternalUser = flagService.internalUser; final ownedFilesCount = split.ownedByCurrentUser.length; final ownedAndPendingUploadFilesCount = ownedFilesCount + split.pendingUploads.length; diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index 98ed03f7a..444afdbe6 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -18,8 +18,8 @@ import 'package:photos/models/file/trash_file.dart'; import 'package:photos/models/ignored_file.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; +import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/hidden_service.dart'; import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/local_sync_service.dart'; @@ -141,8 +141,7 @@ class FileAppBarState extends State { ); } // only show fav option for files owned by the user - if ((isOwnedByUser || - FeatureFlagService.instance.isInternalUserOrDebugBuild()) && + if ((isOwnedByUser || flagService.internalUser) && !isFileHidden && isFileUploaded) { _actions.add( diff --git a/mobile/lib/ui/viewer/file/video_widget.dart b/mobile/lib/ui/viewer/file/video_widget.dart index c9c07df5c..7f9218e9a 100644 --- a/mobile/lib/ui/viewer/file/video_widget.dart +++ b/mobile/lib/ui/viewer/file/video_widget.dart @@ -9,7 +9,7 @@ import 'package:photos/core/constants.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; -import "package:photos/services/feature_flag_service.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/services/files_service.dart'; import "package:photos/ui/actions/file/file_actions.dart"; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; @@ -161,8 +161,7 @@ class _VideoWidgetState extends State { } }).onError( (error, stackTrace) { - if (mounted && - FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + if (mounted && flagService.internalUser) { if (error is Exception) { showErrorDialogForException( context: context, diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 1026bd7fd..1f9fb0bbb 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -19,8 +19,8 @@ import 'package:photos/models/device_collection.dart'; import 'package:photos/models/gallery_type.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; +import 'package:photos/service_locator.dart'; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; @@ -96,7 +96,7 @@ class _GalleryAppBarWidgetState extends State { _selectedFilesListener = () { setState(() {}); }; - isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild(); + isInternalUser = flagService.internalUser; collectionActions = CollectionActions(CollectionsService.instance); widget.selectedFiles.addListener(_selectedFilesListener); _userAuthEventSubscription = diff --git a/mobile/lib/utils/dialog_util.dart b/mobile/lib/utils/dialog_util.dart index f9bd733ae..f6e9eb021 100644 --- a/mobile/lib/utils/dialog_util.dart +++ b/mobile/lib/utils/dialog_util.dart @@ -5,7 +5,7 @@ import "package:flutter/services.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/button_result.dart'; import 'package:photos/models/typedefs.dart'; -import "package:photos/services/feature_flag_service.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/theme/colors.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/progress_dialog.dart'; @@ -91,8 +91,7 @@ String parseErrorForUI( } } // return generic error if the user is not internal and the error is not in debug mode - if (!(FeatureFlagService.instance.isInternalUserOrDebugBuild() && - kDebugMode)) { + if (!(flagService.internalUser && kDebugMode)) { return genericError; } String errorInfo = ""; diff --git a/mobile/lib/utils/multipart_upload_util.dart b/mobile/lib/utils/multipart_upload_util.dart index 6e0eda8ca..102c08d8d 100644 --- a/mobile/lib/utils/multipart_upload_util.dart +++ b/mobile/lib/utils/multipart_upload_util.dart @@ -6,7 +6,7 @@ import "package:dio/dio.dart"; import "package:logging/logging.dart"; import "package:photos/core/constants.dart"; import "package:photos/core/network/network.dart"; -import "package:photos/services/feature_flag_service.dart"; +import "package:photos/service_locator.dart"; import "package:photos/utils/xml_parser_util.dart"; final _enteDio = NetworkClient.instance.enteDio; @@ -58,7 +58,7 @@ Future calculatePartCount(int fileSize) async { Future getMultipartUploadURLs(int count) async { try { assert( - FeatureFlagService.instance.isInternalUserOrDebugBuild(), + flagService.internalUser, "Multipart upload should not be enabled for external users.", ); final response = await _enteDio.get( diff --git a/mobile/plugins/ente_feature_flag/.metadata b/mobile/plugins/ente_feature_flag/.metadata new file mode 100644 index 000000000..9fc7ede54 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_feature_flag/analysis_options.yaml b/mobile/plugins/ente_feature_flag/analysis_options.yaml new file mode 100644 index 000000000..fac60e247 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml \ No newline at end of file diff --git a/mobile/plugins/ente_feature_flag/lib/ente_feature_flag.dart b/mobile/plugins/ente_feature_flag/lib/ente_feature_flag.dart new file mode 100644 index 000000000..66a7132d8 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/lib/ente_feature_flag.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_feature_flag/lib/src/model.dart b/mobile/plugins/ente_feature_flag/lib/src/model.dart new file mode 100644 index 000000000..49b292148 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/lib/src/model.dart @@ -0,0 +1,66 @@ +import "dart:convert"; +import "dart:io"; + +import "package:flutter/foundation.dart"; + +class RemoteFlags { + final bool enableStripe; + final bool disableCFWorker; + final bool mapEnabled; + final bool faceSearchEnabled; + final bool passKeyEnabled; + final bool recoveryKeyVerified; + final bool internalUser; + final bool betaUser; + + RemoteFlags({ + required this.enableStripe, + required this.disableCFWorker, + required this.mapEnabled, + required this.faceSearchEnabled, + required this.passKeyEnabled, + required this.recoveryKeyVerified, + required this.internalUser, + required this.betaUser, + }); + + static RemoteFlags defaultValue = RemoteFlags( + enableStripe: Platform.isAndroid, + disableCFWorker: false, + mapEnabled: false, + faceSearchEnabled: false, + passKeyEnabled: false, + recoveryKeyVerified: false, + internalUser: kDebugMode, + betaUser: kDebugMode, + ); + + String toJson() => json.encode(toMap()); + Map toMap() { + return { + 'enableStripe': enableStripe, + 'disableCFWorker': disableCFWorker, + 'mapEnabled': mapEnabled, + 'faceSearchEnabled': faceSearchEnabled, + 'passKeyEnabled': passKeyEnabled, + 'recoveryKeyVerified': recoveryKeyVerified, + 'internalUser': internalUser, + 'betaUser': betaUser, + }; + } + + factory RemoteFlags.fromMap(Map map) { + return RemoteFlags( + enableStripe: map['enableStripe'] ?? defaultValue.enableStripe, + disableCFWorker: map['disableCFWorker'] ?? defaultValue.disableCFWorker, + mapEnabled: map['mapEnabled'] ?? defaultValue.mapEnabled, + faceSearchEnabled: + map['faceSearchEnabled'] ?? defaultValue.faceSearchEnabled, + passKeyEnabled: map['passKeyEnabled'] ?? defaultValue.passKeyEnabled, + recoveryKeyVerified: + map['recoveryKeyVerified'] ?? defaultValue.recoveryKeyVerified, + internalUser: map['internalUser'] ?? defaultValue.internalUser, + betaUser: map['betaUser'] ?? defaultValue.betaUser, + ); + } +} diff --git a/mobile/plugins/ente_feature_flag/lib/src/service.dart b/mobile/plugins/ente_feature_flag/lib/src/service.dart new file mode 100644 index 000000000..47539eeb5 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/lib/src/service.dart @@ -0,0 +1,75 @@ +// ignore_for_file: always_use_package_imports + +import "dart:convert"; +import "dart:developer"; +import "dart:io"; + +import "package:dio/dio.dart"; +import "package:flutter/foundation.dart"; +import "package:shared_preferences/shared_preferences.dart"; + +import "model.dart"; + +class FlagService { + final SharedPreferences _prefs; + final Dio _enteDio; + late final bool _usingEnteEmail; + + FlagService(this._prefs, this._enteDio) { + _usingEnteEmail = _prefs.getString("email")?.endsWith("@ente.io") ?? false; + Future.delayed(const Duration(seconds: 5), () { + _fetch(); + }); + } + + RemoteFlags? _flags; + + RemoteFlags get flags { + try { + if (!_prefs.containsKey("remote_flags")) { + _fetch().ignore(); + } + _flags ??= RemoteFlags.fromMap( + jsonDecode(_prefs.getString("remote_flags") ?? "{}"), + ); + return _flags!; + } catch (e) { + debugPrint("Failed to get feature flags $e"); + return RemoteFlags.defaultValue; + } + } + + Future _fetch() async { + try { + if (!_prefs.containsKey("token")) { + log("token not found, skip", name: "FlagService"); + return; + } + log("fetching feature flags", name: "FlagService"); + final response = await _enteDio.get("/remote-store/feature-flags"); + final remoteFlags = RemoteFlags.fromMap(response.data); + await _prefs.setString("remote_flags", remoteFlags.toJson()); + _flags = remoteFlags; + } catch (e) { + debugPrint("Failed to sync feature flags $e"); + } + } + + bool get disableCFWorker => flags.disableCFWorker; + + bool get internalUser => flags.internalUser || _usingEnteEmail || kDebugMode; + + bool get betaUser => flags.betaUser; + + bool get internalOrBetaUser => internalUser || betaUser; + + bool get enableStripe => Platform.isIOS ? false : flags.enableStripe; + + bool get mapEnabled => flags.mapEnabled; + + bool get faceSearchEnabled => flags.faceSearchEnabled; + + bool get passKeyEnabled => flags.passKeyEnabled || internalOrBetaUser; + + bool get recoveryKeyVerified => flags.recoveryKeyVerified; +} diff --git a/mobile/plugins/ente_feature_flag/pubspec.yaml b/mobile/plugins/ente_feature_flag/pubspec.yaml new file mode 100644 index 000000000..7507d61f1 --- /dev/null +++ b/mobile/plugins/ente_feature_flag/pubspec.yaml @@ -0,0 +1,19 @@ +name: ente_feature_flag +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + collection: + dio: ^4.0.6 + flutter: + sdk: flutter + shared_preferences: ^2.0.5 + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: \ No newline at end of file diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index d40f8b0c4..0610e4588 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -434,6 +434,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + ente_feature_flag: + dependency: "direct main" + description: + path: "plugins/ente_feature_flag" + relative: true + source: path + version: "0.0.1" equatable: dependency: "direct main" description: @@ -551,10 +558,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: a864d1b6afd25497a3b57b016886d1763df52baaa69758a46723164de8d187fe + sha256: "6b1152a5af3b1cfe7e45309e96fc1aa14873f410f7aadb3878aa7812acfa7531" url: "https://pub.dev" source: hosted - version: "2.29.0" + version: "2.30.0" firebase_core_platform_interface: dependency: transitive description: @@ -575,10 +582,10 @@ packages: dependency: "direct main" description: name: firebase_messaging - sha256: e41586e0fd04fe9a40424f8b0053d0832e6d04f49e020cdaf9919209a28497e9 + sha256: "87e3eda0ecdfeadb5fd1cf0dc5153aea5307a0cfca751c4b1ac97bfdd805660e" url: "https://pub.dev" source: hosted - version: "14.7.19" + version: "14.8.1" firebase_messaging_platform_interface: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f2f82ee1b..9a2b9f1e2 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -47,6 +47,8 @@ dependencies: dotted_border: ^2.1.0 dropdown_button2: ^2.0.0 email_validator: ^2.0.1 + ente_feature_flag: + path: plugins/ente_feature_flag equatable: ^2.0.5 event_bus: ^2.0.0 exif: ^3.0.0 @@ -59,8 +61,8 @@ dependencies: file_saver: # Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87 git: https://github.com/jesims/file_saver.git - firebase_core: ^2.13.1 - firebase_messaging: ^14.6.2 + firebase_core: ^2.30.0 + firebase_messaging: ^14.8.0 fk_user_agent: ^2.0.1 flutter: sdk: flutter