[mobile] Add feature flag plugin & use ente server to fetch flags. (#1524)
## Description ## Tests
This commit is contained in:
commit
c55272c87e
27 changed files with 273 additions and 220 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<void> _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<void> _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<void> _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
|
||||
|
|
|
@ -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();
|
||||
|
|
28
mobile/lib/service_locator.dart
Normal file
28
mobile/lib/service_locator.dart
Normal file
|
@ -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!;
|
||||
}
|
|
@ -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<EnteFile> filesToCopy;
|
||||
|
|
|
@ -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<void> 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<void> 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<String, dynamic> 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<String, dynamic> json) {
|
||||
return FeatureFlags(
|
||||
disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker,
|
||||
enableStripe: json["enableStripe"] ?? FFDefault.enableStripe,
|
||||
enablePasskey: json["enablePasskey"] ?? FFDefault.enablePasskey,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<SecuritySectionWidget> {
|
|||
final Completer completer = Completer();
|
||||
final List<Widget> children = [];
|
||||
if (_config.hasConfiguredAccount()) {
|
||||
final bool isInternalUser =
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
children.addAll(
|
||||
[
|
||||
sectionOptionSpacing,
|
||||
|
@ -103,8 +101,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (isInternalUser) sectionOptionSpacing,
|
||||
if (isInternalUser)
|
||||
if (flagService.passKeyEnabled) sectionOptionSpacing,
|
||||
if (flagService.passKeyEnabled)
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.passkey,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<AppStorageViewer> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
internalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
internalUser = flagService.internalUser;
|
||||
addPath();
|
||||
super.initState();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<FileAppBar> {
|
|||
);
|
||||
}
|
||||
// only show fav option for files owned by the user
|
||||
if ((isOwnedByUser ||
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild()) &&
|
||||
if ((isOwnedByUser || flagService.internalUser) &&
|
||||
!isFileHidden &&
|
||||
isFileUploaded) {
|
||||
_actions.add(
|
||||
|
|
|
@ -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<VideoWidget> {
|
|||
}
|
||||
}).onError(
|
||||
(error, stackTrace) {
|
||||
if (mounted &&
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
|
||||
if (mounted && flagService.internalUser) {
|
||||
if (error is Exception) {
|
||||
showErrorDialogForException(
|
||||
context: context,
|
||||
|
|
|
@ -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<GalleryAppBarWidget> {
|
|||
_selectedFilesListener = () {
|
||||
setState(() {});
|
||||
};
|
||||
isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
isInternalUser = flagService.internalUser;
|
||||
collectionActions = CollectionActions(CollectionsService.instance);
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
_userAuthEventSubscription =
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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<int> calculatePartCount(int fileSize) async {
|
|||
Future<MultipartUploadURLs> 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(
|
||||
|
|
10
mobile/plugins/ente_feature_flag/.metadata
Normal file
10
mobile/plugins/ente_feature_flag/.metadata
Normal file
|
@ -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
|
1
mobile/plugins/ente_feature_flag/analysis_options.yaml
Normal file
1
mobile/plugins/ente_feature_flag/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: ../../analysis_options.yaml
|
|
@ -0,0 +1 @@
|
|||
export 'src/service.dart';
|
66
mobile/plugins/ente_feature_flag/lib/src/model.dart
Normal file
66
mobile/plugins/ente_feature_flag/lib/src/model.dart
Normal file
|
@ -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<String, dynamic> toMap() {
|
||||
return {
|
||||
'enableStripe': enableStripe,
|
||||
'disableCFWorker': disableCFWorker,
|
||||
'mapEnabled': mapEnabled,
|
||||
'faceSearchEnabled': faceSearchEnabled,
|
||||
'passKeyEnabled': passKeyEnabled,
|
||||
'recoveryKeyVerified': recoveryKeyVerified,
|
||||
'internalUser': internalUser,
|
||||
'betaUser': betaUser,
|
||||
};
|
||||
}
|
||||
|
||||
factory RemoteFlags.fromMap(Map<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
75
mobile/plugins/ente_feature_flag/lib/src/service.dart
Normal file
75
mobile/plugins/ente_feature_flag/lib/src/service.dart
Normal file
|
@ -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<void> _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;
|
||||
}
|
19
mobile/plugins/ente_feature_flag/pubspec.yaml
Normal file
19
mobile/plugins/ente_feature_flag/pubspec.yaml
Normal file
|
@ -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:
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue