diff --git a/lib/about_page.dart b/lib/about_page.dart index f98ad07d..6709c4dd 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -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)( diff --git a/lib/core/state.dart b/lib/core/state.dart index af488d68..540e2f01 100644 --- a/lib/core/state.dart +++ b/lib/core/state.dart @@ -53,17 +53,21 @@ abstract class ApplicationStateNotifier } // 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()); + +class FeatureFlagsNotifier extends StateNotifier> { + FeatureFlagsNotifier() : super({}); + + void loadConfig(Map config) { + const falsey = [0, false, null]; + state = {for (final k in config.keys) k: !falsey.contains(config[k])}; + } +} + final featureProvider = Provider((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; }); diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index d7672be6..6cc5b21e 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -157,6 +157,11 @@ Future initialize(List 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 initialize(List 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(), + )); }), ), ), diff --git a/lib/piv/features.dart b/lib/piv/features.dart index 94394e38..a96629e6 100644 --- a/lib/piv/features.dart +++ b/lib/piv/features.dart @@ -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');