high priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Tapping anywhere on the banner body (excluding the dismiss button) calls scenario-deep-link-router (578) with the scenario type and the full contextual payload from the notification
The banner is dismissed (removed from the Overlay) immediately and synchronously before the navigation call completes, so the user never sees the banner lingering on the destination screen
The dismiss button tap dismisses the banner without triggering navigation
scenario-deep-link-router (578) correctly receives and uses the scenario_type field to resolve the correct destination (e.g., activity_wizard for activity-prompt scenarios)
If scenario-deep-link-router (578) returns an error or unknown route, the banner is still dismissed and a fallback navigation (e.g., home screen) is performed rather than leaving the user on the current screen with no feedback
Navigation is idempotent: rapid double-taps do not push the destination screen twice onto the navigator stack
The tap handler does not interfere with the auto-dismiss timer cancellation from task-006; both concerns work correctly when the user taps (timer cancelled + banner dismissed + navigation triggered)
Deep-link router call is made using the correct context (navigator context, not the overlay entry context) to avoid navigating in a detached navigator

Technical Requirements

frameworks
Flutter (GestureDetector or InkWell for tap detection)
Riverpod (to access scenario-deep-link-router (578) via ref.read in the tap handler)
Flutter Navigator 2.0 / GoRouter (whichever the project uses for routing)
apis
scenario-deep-link-router (578) — navigate(ScenarioType type, Map<String, dynamic> payload) method
data models
scenario_notifications (scenario_type and contextual payload fields passed to router)
performance requirements
Banner dismissal must be synchronous (single frame) before navigation begins
Navigation must not be called twice — use a boolean flag or setState guard to prevent double-tap race conditions
security requirements
Contextual payload passed to the router must be validated before use — reject payloads with unexpected keys or types to prevent injection into navigation arguments
Navigation target must be resolved server-side or via a fixed enum map — never construct route strings directly from unvalidated notification payload strings
ui components
GestureDetector or InkWell wrapping the banner body (excluding dismiss button area)
Dismiss button remains a separate tap target with its own onTap that only dismisses

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Add an optional onTap callback to the banner widget constructor (onTap: VoidCallback?) — the widget itself does not know about the router; the OverlayController or the Riverpod consumer that creates the OverlayEntry supplies the onTap closure that calls ref.read(scenarioDeepLinkRouterProvider).navigate(...). This keeps the widget decoupled from routing. To prevent double-navigation, use a bool _navigating flag in the OverlayController and set it to true before calling navigate. Always dismiss (remove the OverlayEntry) before calling the router so the banner is gone regardless of whether navigation succeeds.

Ensure the navigator context used in the router call is the root navigator context stored at app startup, not the overlay entry's BuildContext, which may be unmounted by the time the async navigation resolves.

Testing Requirements

Write widget tests: tap the banner body and assert the mock router was called with the correct scenario_type and payload; assert the banner is no longer in the widget tree after the tap. Tap the dismiss button and assert the router was NOT called. Simulate a double-tap and assert router.navigate was called exactly once. Test the fallback: configure the mock router to throw an UnknownRouteException and assert the banner is still dismissed.

Write a unit test for the tap guard (boolean flag) in the handler function to confirm double-call protection. Manual test on device: verify the activity wizard opens when tapping a notification banner with scenario_type = 'activity_prompt'.

Component
In-App Notification Banner
ui low
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.