high priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

One Switch widget (or equivalent) is rendered per supported scenario type — each is visually distinct and labelled with the scenario name in the active locale
Each toggle row has a minimum touch target of 44x44 logical pixels, verified by a widget test using tester.getSize()
The Switch active (on) colour passes WCAG 2.2 AA contrast ratio of at least 3:1 against the background colour — verified by a manual contrast check or automated tool on the design token values
Each toggle is wrapped in a Semantics widget with: label set to the scenario name in the active locale, value set to 'on' or 'off' matching the current state, and onTap set so screen readers can activate the toggle
VoiceOver on iOS and TalkBack on Android announce the correct label and state when focus lands on a toggle — verified by manual accessibility audit on device
Toggling a switch updates the local UI state immediately (optimistic update) without waiting for a backend call
Toggle state changes are propagated to a BLoC or Riverpod provider — the state management layer receives the updated preference value
A disabled/loading state is supported: when preferences are being saved, toggles show a visual loading indicator and are non-interactive
All scenario types defined in the ScenarioType enum are represented — adding a new enum value does not require manual UI changes (driven by the enum)
Running `flutter test` on widget tests covering all toggle interactions passes without overflow or accessibility warnings

Technical Requirements

frameworks
Flutter
BLoC or Riverpod
flutter_test
apis
NotificationPreferencesRepository (read/write per-scenario toggle state)
Supabase (via repository, for persisting preferences)
data models
ScenarioType enum
NotificationPreference (scenarioType, isEnabled)
NotificationPreferencesState (BLoC state or Riverpod AsyncValue)
performance requirements
Toggle state change must update the UI within one frame (16ms) — no perceptible lag on tap
Preferences are persisted asynchronously — the UI must not block on the Supabase write
security requirements
Preference writes to Supabase must be scoped to the authenticated user's ID — never write preferences for another user's ID
Row-Level Security (RLS) on the notification_preferences table must prevent cross-user reads/writes — verify this in the integration test
ui components
Switch (Flutter Material or Cupertino depending on platform design)
Semantics wrapper per toggle row
ListTile or custom Row with minimum 44x44 pt touch target
CircularProgressIndicator or Shimmer for loading state
Section container from task-001 scaffold

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Drive the toggle list from ScenarioType.values — iterate the enum to build toggle rows so the UI is automatically updated when new scenario types are added. Each toggle row should be a reusable ScenarioToggleListTile widget (stateless, accepting scenarioType, isEnabled, onChanged) to keep NotificationPreferencesScreen clean. For WCAG 2.2 AA contrast on the Switch: Flutter's default Switch uses the theme's colorScheme.primary for the active track — ensure the design token for primary colour has at least 3:1 contrast against the card background. Set the Switch's materialTapTargetSize: MaterialTapTargetSize.padded or wrap in a SizedBox(height: 44, width: 44) to guarantee minimum touch target.

For Semantics: use Semantics(label: scenarioName, value: isEnabled ? 'on' : 'off', child: Switch(...)) — do not rely on Flutter's default Switch semantics as they may not announce the scenario name. The disabled/loading state should be managed by the BLoC/Riverpod state machine, not local widget state, to keep the screen a pure view layer.

Testing Requirements

Widget tests using flutter_test. Pump the NotificationPreferencesScreen with a mocked BLoC/Riverpod provider pre-loaded with a known preference state. Test: (1) correct number of toggles rendered matches ScenarioType.values.length, (2) each toggle reflects the correct initial on/off state from the mock provider, (3) tapping a toggle dispatches the correct event/action to the provider, (4) tapping a toggle updates Switch.value in the widget tree, (5) minimum touch target size via tester.getSize() >= Size(44, 44). Add a separate accessibility test using flutter_test's SemanticsHandle to verify Semantics labels and values are correctly set for each toggle.

Run `flutter analyze` with zero warnings.

Epic Risks (2)
medium impact medium prob technical

The in-app notification banner depends on a Supabase Realtime subscription to detect new notification records. If the subscription reconnects slowly after an app resume from background, or if Realtime delivery is delayed under high load, the banner may not appear within the 2-second acceptance criterion.

Mitigation & Contingency

Mitigation: Implement an explicit subscription reconnect handler on app foreground events using Flutter's AppLifecycleState.resumed hook, and add a polling fallback that queries for unread notifications once per app foreground event as a safety net against missed Realtime events.

Contingency: If Realtime proves unreliable in production, promote the polling fallback to the primary mechanism with a 30-second interval, accepting slight latency in exchange for reliability.

medium impact medium prob technical

Cold-start deep linking (app not running when push notification is tapped) requires deferred navigation after the Flutter engine and Supabase session are fully initialised. If the deep link is consumed before authentication completes, the router may navigate to a protected route without a valid session, causing an error or redirect loop.

Mitigation & Contingency

Mitigation: Implement a deferred navigation queue in scenario-deep-link-router that holds the parsed deep-link target until the auth session restoration lifecycle event fires, following the existing deep-link-handler pattern used in the BankID and Vipps authentication flows.

Contingency: If deferred navigation is not achievable within the epic's scope, fall back to navigating the user to the notification centre (which is always accessible post-login) where the relevant notification record is visible and tappable.