Skip to content

Commit 159581f

Browse files
committed
feat: Enhance feature flag display with source indicators and add refresh confirmation.
also implement firebase native way to process default value
1 parent dd310f0 commit 159581f

File tree

11 files changed

+241
-49
lines changed

11 files changed

+241
-49
lines changed

assets/feature_flags.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"dark",
1111
"system"
1212
]
13-
}
13+
},
14+
"_force_override_flags": ""
1415
}

lib/i18n/en-US.i18n.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ profile:
103103
clearPreferences: Clear Preferences
104104
clearCredentials: Clear Credentials
105105
clearUserData: Clear User Data
106-
cleared: '${item} cleared'
106+
cleared: "${item} cleared"
107107
clearFailed: Failed to clear ${item}
108108
items:
109109
cache: Cache
@@ -128,3 +128,9 @@ about:
128128
featureFlags:
129129
title: Feature Flags
130130
reset: Reset to default
131+
status:
132+
defaultStatus: Default
133+
remote: Remote
134+
overrideStatus: Override
135+
forced: Forced
136+
refreshed: Refreshed from remote

lib/i18n/strings.g.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/// To regenerate, run: `dart run slang`
55
///
66
/// Locales: 2
7-
/// Strings: 216 (108 per locale)
7+
/// Strings: 226 (113 per locale)
88
99
// coverage:ignore-file
1010
// ignore_for_file: type=lint, unused_import

lib/i18n/strings_en_US.g.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ class _TranslationsFeatureFlagsEnUs extends TranslationsFeatureFlagsZhTw {
206206
// Translations
207207
@override String get title => 'Feature Flags';
208208
@override String get reset => 'Reset to default';
209+
@override late final _TranslationsFeatureFlagsStatusEnUs status = _TranslationsFeatureFlagsStatusEnUs._(_root);
210+
@override String get refreshed => 'Refreshed from remote';
209211
}
210212

211213
// Path: intro.features
@@ -314,6 +316,19 @@ class _TranslationsProfileDangerZoneEnUs extends TranslationsProfileDangerZoneZh
314316
@override late final _TranslationsProfileDangerZoneItemsEnUs items = _TranslationsProfileDangerZoneItemsEnUs._(_root);
315317
}
316318

319+
// Path: featureFlags.status
320+
class _TranslationsFeatureFlagsStatusEnUs extends TranslationsFeatureFlagsStatusZhTw {
321+
_TranslationsFeatureFlagsStatusEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root);
322+
323+
final TranslationsEnUs _root; // ignore: unused_field
324+
325+
// Translations
326+
@override String get defaultStatus => 'Default';
327+
@override String get remote => 'Remote';
328+
@override String get overrideStatus => 'Override';
329+
@override String get forced => 'Forced';
330+
}
331+
317332
// Path: intro.features.courseTable
318333
class _TranslationsIntroFeaturesCourseTableEnUs extends TranslationsIntroFeaturesCourseTableZhTw {
319334
_TranslationsIntroFeaturesCourseTableEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root);
@@ -477,6 +492,11 @@ extension on TranslationsEnUs {
477492
'about.copyright' => '© 2025 NTUT Programming Club\nLicensed under the GNU GPL v3.0',
478493
'featureFlags.title' => 'Feature Flags',
479494
'featureFlags.reset' => 'Reset to default',
495+
'featureFlags.status.defaultStatus' => 'Default',
496+
'featureFlags.status.remote' => 'Remote',
497+
'featureFlags.status.overrideStatus' => 'Override',
498+
'featureFlags.status.forced' => 'Forced',
499+
'featureFlags.refreshed' => 'Refreshed from remote',
480500
_ => null,
481501
};
482502
}

lib/i18n/strings_zh_TW.g.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ class TranslationsFeatureFlagsZhTw {
297297

298298
/// zh-TW: '恢復預設值'
299299
String get reset => '恢復預設值';
300+
301+
late final TranslationsFeatureFlagsStatusZhTw status = TranslationsFeatureFlagsStatusZhTw.internal(_root);
302+
303+
/// zh-TW: '已從遠端截取'
304+
String get refreshed => '已從遠端截取';
300305
}
301306

302307
// Path: intro.features
@@ -485,6 +490,27 @@ class TranslationsProfileDangerZoneZhTw {
485490
late final TranslationsProfileDangerZoneItemsZhTw items = TranslationsProfileDangerZoneItemsZhTw.internal(_root);
486491
}
487492

493+
// Path: featureFlags.status
494+
class TranslationsFeatureFlagsStatusZhTw {
495+
TranslationsFeatureFlagsStatusZhTw.internal(this._root);
496+
497+
final Translations _root; // ignore: unused_field
498+
499+
// Translations
500+
501+
/// zh-TW: '預設'
502+
String get defaultStatus => '預設';
503+
504+
/// zh-TW: '遠端'
505+
String get remote => '遠端';
506+
507+
/// zh-TW: '覆寫'
508+
String get overrideStatus => '覆寫';
509+
510+
/// zh-TW: '強制'
511+
String get forced => '強制';
512+
}
513+
488514
// Path: intro.features.courseTable
489515
class TranslationsIntroFeaturesCourseTableZhTw {
490516
TranslationsIntroFeaturesCourseTableZhTw.internal(this._root);
@@ -670,6 +696,11 @@ extension on Translations {
670696
'about.copyright' => '© 2025北科程式設計研究社\n以GNU GPL v3.0授權條款釋出',
671697
'featureFlags.title' => '功能開關',
672698
'featureFlags.reset' => '恢復預設值',
699+
'featureFlags.status.defaultStatus' => '預設',
700+
'featureFlags.status.remote' => '遠端',
701+
'featureFlags.status.overrideStatus' => '覆寫',
702+
'featureFlags.status.forced' => '強制',
703+
'featureFlags.refreshed' => '已從遠端截取',
673704
_ => null,
674705
};
675706
}

lib/i18n/zh-TW.i18n.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,9 @@ about:
128128
featureFlags:
129129
title: 功能開關
130130
reset: 恢復預設值
131+
status:
132+
defaultStatus: 預設
133+
remote: 遠端
134+
overrideStatus: 覆寫
135+
forced: 強制
136+
refreshed: 已從遠端截取

lib/repositories/feature_flag_providers.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ class FeatureFlagsNotifier extends AsyncNotifier<List<FeatureFlag>> {
1616
Future<void> refreshFlags() async {
1717
state = const AsyncValue.loading();
1818
state = await AsyncValue.guard(() {
19-
return ref.read(featureFlagRepositoryProvider).getAllFlags(forceRefresh: true);
19+
return ref
20+
.read(featureFlagRepositoryProvider)
21+
.getAllFlags(forceRefresh: true);
2022
});
2123
}
2224

lib/repositories/feature_flag_repository.dart

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@ import 'package:shared_preferences/shared_preferences.dart';
55
import 'package:tattoo/services/feature_flag/feature_flag_service.dart';
66
import 'package:tattoo/services/firebase_service.dart';
77

8+
enum FeatureFlagSource {
9+
local,
10+
remote,
11+
override,
12+
forced,
13+
}
14+
815
/// The model for a UI-consumable feature flag.
916
class FeatureFlag {
1017
final String key;
1118
final dynamic defaultValue;
1219
final dynamic overrideValue;
1320
final List<dynamic>? options;
1421
final bool isForced;
22+
final bool isRemote;
1523

1624
const FeatureFlag({
1725
required this.key,
1826
required this.defaultValue,
1927
this.overrideValue,
2028
this.options,
2129
this.isForced = false,
30+
this.isRemote = false,
2231
});
2332

2433
dynamic get value => overrideValue ?? defaultValue;
2534
Type get type => defaultValue.runtimeType;
2635

36+
FeatureFlagSource get source {
37+
if (isForced) return FeatureFlagSource.forced;
38+
if (overrideValue != null) return FeatureFlagSource.override;
39+
if (isRemote) return FeatureFlagSource.remote;
40+
return FeatureFlagSource.local;
41+
}
42+
2743
// Type-safe getters
2844
bool get asBool => value as bool;
2945
int get asInt => value as int;
@@ -42,15 +58,17 @@ final featureFlagRepositoryProvider = Provider<FeatureFlagRepository>((ref) {
4258
class FeatureFlagRepository {
4359
final FeatureFlagService _service;
4460
final SharedPreferencesAsync _prefs;
45-
Map<String, dynamic>? _defaultsCache;
61+
Map<String, FeatureFlagData>? _defaultsCache;
4662

4763
FeatureFlagRepository({
4864
required FeatureFlagService service,
4965
required SharedPreferencesAsync prefs,
5066
}) : _service = service,
5167
_prefs = prefs;
5268

53-
Future<Map<String, dynamic>> _getDefaults({bool forceRefresh = false}) async {
69+
Future<Map<String, FeatureFlagData>> _getDefaults({
70+
bool forceRefresh = false,
71+
}) async {
5472
if (forceRefresh) _defaultsCache = null;
5573
return _defaultsCache ??= await _service.fetchDefaultFlags();
5674
}
@@ -59,7 +77,8 @@ class FeatureFlagRepository {
5977
final defaults = await _getDefaults(forceRefresh: forceRefresh);
6078
final list = <FeatureFlag>[];
6179

62-
final forceOverrideString = defaults['_force_override_flags'] as String?;
80+
final forceOverrideString =
81+
defaults['_force_override_flags']?.value as String?;
6382
final forceOverrides =
6483
forceOverrideString
6584
?.split(',')
@@ -72,7 +91,8 @@ class FeatureFlagRepository {
7291
final key = entry.key;
7392
if (key == '_force_override_flags') continue;
7493

75-
dynamic defVal = entry.value;
94+
dynamic defVal = entry.value.value;
95+
final isRemote = entry.value.isRemote;
7696
List<dynamic>? options;
7797

7898
if (defVal is Map<String, dynamic> &&
@@ -106,6 +126,7 @@ class FeatureFlagRepository {
106126
overrideValue: overrideVal,
107127
options: options,
108128
isForced: isForced,
129+
isRemote: isRemote,
109130
),
110131
);
111132
}

0 commit comments

Comments
 (0)