Implement per-scenario toggle controls with WCAG 2.2 AA
epic-scenario-push-engagement-ui-task-002 — Add one toggle switch per supported scenario type (e.g., days-inactive, certification-expiry, session-count) inside the notification-preferences-screen. Each toggle must meet WCAG 2.2 AA contrast and minimum 44x44 pt touch target requirements. Use Semantics wrappers for screen-reader labels so VoiceOver and TalkBack announce the scenario name and current on/off state.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.