high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

markAsRead is called automatically on screen mount (initState or equivalent) without requiring any user interaction
markAsRead failure is caught silently — it must never block the user from viewing the notification or tapping the CTA; an optional non-intrusive log or analytics event may be emitted
The unread badge count in the notification list is updated reactively after markAsRead succeeds (Riverpod invalidation or equivalent)
The CTA button is rendered with a minimum touch target of 48Ă—48 pt (using SizedBox or equivalent padding wrapper)
The CTA button has a semanticLabel set to a localised accessible string describing the destination action (e.g., 'Go to certification details')
Tapping the CTA button invokes scenario-deep-link-router (578) with the correct ScenarioType enum value and the notification payload object
If scenario-deep-link-router throws a RouteNotFoundException or unknown scenario type, the screen shows a localised error snackbar and does not crash
While the deep-link router is resolving (if async), the CTA button shows a loading indicator and is disabled to prevent double-taps
The CTA button is not rendered when the notification has no actionable payload (payload is null or scenario type is 'informational'); an informational-only layout is shown instead
Screen is fully functional with VoiceOver/TalkBack: the CTA button is announced with its accessible label, and focus order is logical (content rows → CTA button)

Technical Requirements

frameworks
Flutter
Riverpod
apis
scenario-notification-repository (579) — markAsRead(notificationId)
scenario-deep-link-router (578) — navigate(scenarioType, payload)
data models
ScenarioNotification
ScenarioType (enum)
ScenarioPayload
performance requirements
markAsRead must be a fire-and-forget call (unawaited or equivalent) so it does not delay screen render
CTA navigation must initiate within 100ms of button tap on the UI thread
security requirements
The payload forwarded to scenario-deep-link-router must not be modified or augmented at this layer; pass through exactly as stored in the notification record
Do not log payload contents at verbose level if they contain user-identifiable scenario data
ui components
AppButton (primary variant, minimum 48Ă—48 pt, semanticLabel prop)
LoadingOverlay or ButtonLoadingIndicator for async CTA state
SnackBar (error feedback for router failures)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Call markAsRead in initState using ref.read (not ref.watch) to avoid re-triggering on rebuild. Use unawaited() explicitly and wrap in a try/catch to silence errors per the acceptance criteria. For the CTA, use an AsyncNotifier or local ValueNotifier isLoading flag to manage button state during router resolution. The scenario-deep-link-router call should be awaited so you can catch errors and show the snackbar — but ensure the await does not block UI with a visible delay beyond the button loading state.

Use Semantics widget wrapping AppButton if AppButton does not natively support semanticLabel prop. Follow the existing AppButton widget API in the shared widget library; do not create a one-off button implementation. Keep the mark-as-read logic in the screen's initState rather than a side-effect in the Riverpod provider to maintain clear separation of concerns.

Testing Requirements

Write flutter_test widget tests covering: (1) markAsRead is called once on screen mount (verify via mock); (2) markAsRead failure does not prevent screen display or CTA interaction; (3) CTA button tap invokes scenario-deep-link-router with correct arguments; (4) CTA button is disabled and shows loading indicator during async router resolution; (5) router failure shows error snackbar; (6) CTA button is absent when payload is null. Mock both scenario-notification-repository and scenario-deep-link-router. Verify semanticLabel presence via SemanticsNode in widget tests. Integration test for the full banner→detail→deep-link path is handled in task-012.

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.