final class FeatureSchedulingGuard
extends PresentumGuard<FeatureItem, AppSurface, AppVariant> {
FeatureSchedulingGuard({
required this.eligibilityResolver,
super.refresh,
});
final EligibilityResolver<FeatureItem> eligibilityResolver;
@override
Future<PresentumState<FeatureItem, AppSurface, AppVariant>> call(
PresentumStorage<AppSurface, AppVariant> storage,
List<PresentumHistoryEntry<FeatureItem, AppSurface, AppVariant>> history,
PresentumState$Mutable<FeatureItem, AppSurface, AppVariant> state,
List<FeatureItem> candidates,
Map<String, Object?> context,
) async {
// 1) Filter: if feature is gone, disabled, ineligible, or dismissed,
// exclude it from UI.
final filtered = <FeatureItem>[];
for (final item in candidates) {
// Check eligibility (time ranges, segments, etc.)
final isEligible = await eligibilityResolver.isEligible(item, context);
if (!isEligible) continue;
// Check if feature is dismissed
final dismissedAt = await storage.getDismissedAt(
item.id,
surface: item.surface,
variant: item.variant,
);
if (dismissedAt != null) continue;
filtered.add(item);
}
// 3) Project candidates -> slots (active + queue)
// This is what makes Settings rows and UI banners actually appear.
state.clearAll();
final bySurface = <AppSurface, List<FeatureItem>>{};
for (final item in filtered) {
(bySurface[item.surface] ??= <FeatureItem>[]).add(item);
}
for (final entry in bySurface.entries) {
final surface = entry.key;
final items = entry.value;
int stageOf(FeatureItem i) => i.stage ?? 0;
items.sort((a, b) {
final stageCmp = stageOf(a).compareTo(stageOf(b));
if (stageCmp != 0) return stageCmp;
return b.priority.compareTo(a.priority);
});
state.addAll(surface, items);
}
return state;
}
}