final class FeatureDrivenProvider extends ChangeNotifier {
FeatureDrivenProvider({
required this.engine,
required this.catalog,
required this.prefs,
}) {
catalog.addListener(_sync);
// Initial sync so the Settings list is immediately populated.
Future.microtask(_sync);
}
final PresentumEngine<FeatureItem, AppSurface, AppVariant> engine;
final FeatureCatalogStore catalog;
final FeaturePreferencesStore prefs;
Future<void> _sync() async {
// If features were removed upstream, prune user overrides too.
await prefs.pruneTo(catalog.features.keys.toSet());
final candidates = <FeatureItem>[];
// 1) Settings: one row per feature that exists right now.
for (final feature in catalog.features.values) {
final payload = FeaturePayload(
id: 'settings_toggle:${feature.key}',
featureKey: feature.key,
priority: 0,
metadata: {
// Localization keys (UI resolves them to strings).
'titleKey': feature.titleKey,
'descriptionKey': feature.descriptionKey,
},
options: const [
FeatureOption(
surface: AppSurface.settingsToggles,
variant: AppVariant.settingToggleRow,
isDismissible: false,
stage: null, // stage comes from catalog order (below)
alwaysOnIfEligible: true,
),
],
);
for (final opt in payload.options) {
// Use option.stage for ordering (Settings ordering is catalog-driven).
final orderedOpt = FeatureOption(
surface: opt.surface,
variant: opt.variant,
isDismissible: opt.isDismissible,
stage: feature.order,
maxImpressions: opt.maxImpressions,
cooldownMinutes: opt.cooldownMinutes,
alwaysOnIfEligible: opt.alwaysOnIfEligible,
);
candidates.add(FeatureItem(payload: payload, option: orderedOpt));
}
}
// 2) Example “real UI”: a promo banner that belongs to a feature.
// If the feature is removed from the catalog, it disappears everywhere
// because the provider stops emitting it.
if (catalog.exists('new_year_theme')) {
final payload = FeaturePayload(
id: 'banner:new_year_theme',
featureKey: 'new_year_theme',
priority: 50,
metadata: {'title': 'Holiday theme enabled'},
options: const [
FeatureOption(
surface: AppSurface.homeHeader,
variant: AppVariant.banner,
isDismissible: true,
alwaysOnIfEligible: true,
),
],
);
for (final opt in payload.options) {
candidates.add(FeatureItem(payload: payload, option: opt));
}
}
await engine.setCandidatesWithDiff((state) => candidates);
notifyListeners();
}
@override
void dispose() {
catalog.removeListener(_sync);
super.dispose();
}
}