Refactor feature flag support.

This commit is contained in:
Dain Nilsson 2023-10-04 11:08:02 +02:00
parent 75f8f5be35
commit 5d9420f47f
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
4 changed files with 67 additions and 16 deletions

View File

@ -165,6 +165,7 @@ class AboutPage extends ConsumerWidget {
'os': Platform.operatingSystem,
'os_version': Platform.operatingSystemVersion,
});
data.insert(data.length - 1, ref.read(featureFlagProvider));
final text = const JsonEncoder.withIndent(' ').convert(data);
await ref.read(clipboardProvider).setText(text);
await ref.read(withContextProvider)(

View File

@ -53,17 +53,21 @@ abstract class ApplicationStateNotifier<T>
}
// Feature flags
abstract class BaseFeature {
sealed class BaseFeature {
String get path;
String _subpath(String key);
Feature feature(String key, {bool enabled = true}) =>
Feature(this, key, enabled: enabled);
Feature._(this, key, enabled: enabled);
}
class _RootFeature extends BaseFeature {
_RootFeature._();
@override
String get path => '';
@override
String _subpath(String key) => key;
}
class Feature extends BaseFeature {
@ -71,18 +75,41 @@ class Feature extends BaseFeature {
final String key;
final bool _defaultState;
Feature(this.parent, this.key, {bool enabled = true})
Feature._(this.parent, this.key, {bool enabled = true})
: _defaultState = enabled;
@override
String get path => '${parent.path}.$key';
String get path => parent._subpath(key);
@override
String _subpath(String key) => '$path.$key';
}
final BaseFeature root = _RootFeature._();
typedef FeatureProvider = bool Function(Feature feature);
final featureFlagProvider =
StateNotifierProvider<FeatureFlagsNotifier, Map<String, bool>>(
(_) => FeatureFlagsNotifier());
class FeatureFlagsNotifier extends StateNotifier<Map<String, bool>> {
FeatureFlagsNotifier() : super({});
void loadConfig(Map<String, dynamic> config) {
const falsey = [0, false, null];
state = {for (final k in config.keys) k: !falsey.contains(config[k])};
}
}
final featureProvider = Provider<FeatureProvider>((ref) {
// TODO: Read file, check parents
return (feature) => feature._defaultState;
final featureMap = ref.watch(featureFlagProvider);
bool isEnabled(BaseFeature feature) => switch (feature) {
_RootFeature() => true,
Feature() => isEnabled(feature.parent) &&
(featureMap[feature.path] ?? feature._defaultState),
};
return isEnabled;
});

View File

@ -157,6 +157,11 @@ Future<Widget> initialize(List<String> argv) async {
.toFilePath();
}
// Locate feature flags file
final featureFile = File(Uri.file(Platform.resolvedExecutable)
.resolve('features.json')
.toFilePath());
final rpcFuture = _initHelper(exe!);
_initLicenses();
@ -218,15 +223,33 @@ Future<Widget> initialize(List<String> argv) async {
ref.read(rpcProvider).valueOrNull?.setLogLevel(level);
});
// Load feature flags, if they exist
featureFile.exists().then(
(exists) async {
if (exists) {
try {
final featureConfig =
jsonDecode(await featureFile.readAsString());
ref
.read(featureFlagProvider.notifier)
.loadConfig(featureConfig);
} catch (error) {
_log.error('Failed to parse feature flags', error);
}
}
},
);
// Initialize systray
ref.watch(systrayProvider);
// Show a loading or error page while the Helper isn't ready
return ref.watch(rpcProvider).when(
data: (data) => const MainPage(),
error: (error, stackTrace) => AppFailurePage(cause: error),
loading: () => _HelperWaiter(),
);
return Consumer(
builder: (context, ref, child) => ref.watch(rpcProvider).when(
data: (data) => const MainPage(),
error: (error, stackTrace) => AppFailurePage(cause: error),
loading: () => _HelperWaiter(),
));
}),
),
),

View File

@ -20,12 +20,12 @@ final actions = piv.feature('actions');
final actionsPin = actions.feature('pin');
final actionsPuk = actions.feature('puk');
final actionsManagementKey = actions.feature('managementKey', enabled: false);
final actionsReset = actions.feature('reset', enabled: false);
final actionsManagementKey = actions.feature('managementKey');
final actionsReset = actions.feature('reset');
final slots = piv.feature('slots');
final slotsGenerate = slots.feature('generate', enabled: false);
final slotsImport = slots.feature('import', enabled: false);
final slotsGenerate = slots.feature('generate');
final slotsImport = slots.feature('import');
final slotsExport = slots.feature('export');
final slotsDelete = slots.feature('delete', enabled: false);
final slotsDelete = slots.feature('delete');