high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Banner automatically dismisses after 5 seconds (default, configurable via constructor parameter) from the moment it becomes visible
The auto-dismiss timer is cancelled immediately if the user taps anywhere on the banner (including the dismiss button); the banner then remains visible until explicitly dismissed by the user
Timer countdown does not cause visible UI flicker or unnecessary rebuilds; the banner widget does not show a countdown number unless explicitly configured
When the banner appears, accessibility-live-region-announcer (664) announces the scenario title and short message text to VoiceOver (iOS) and TalkBack (Android) without shifting visual focus away from the user's current position on screen
The announcement uses assertive priority so it interrupts any currently queued accessibility announcement and is read immediately
If the screen reader is not active, the live region announcement has no visible side effects
Auto-dismiss countdown is paused while the device screen reader is active to give visually impaired users adequate time to hear the announcement
If two banners queue, the timer for the second banner starts only after the first banner is fully dismissed (not concurrently)
The configurable dismiss duration accepts values between 3 and 30 seconds; out-of-range values default to 5 seconds without throwing

Technical Requirements

frameworks
Flutter (Timer from dart:async for countdown)
Flutter Semantics API (SemanticsService.announce or Semantics widget with liveRegion: true)
accessibility-live-region-announcer (664) — project-specific announcer service
apis
accessibility-live-region-announcer (664) — announce(String text, TextDirection direction, {bool assertive}) method
dart:async Timer — for auto-dismiss countdown
performance requirements
Timer must use dart:async Timer, not a stream, to avoid over-building the widget
Cancellation of the timer on tap must be synchronous to prevent edge-case double-dismissals
security requirements
Announcement text must not include any PII beyond what is already rendered in the banner — use only scenario title and short message fields
ui components
in-app-notification-banner (573, enhanced from task-004 and task-005)
GestureDetector or InkWell wrapper to detect tap-to-cancel-timer

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Manage the timer in the widget's State (StatefulWidget) or in the OverlayController that manages the OverlayEntry — prefer the controller so the widget itself stays lean. Use SemanticsService.announce(text, textDirection) as the lowest-level Flutter API, or delegate to accessibility-live-region-announcer (664) if the project already provides this abstraction. Do not use Semantics(liveRegion: true) on the banner container itself, as this can cause repeated announcements on every rebuild — call announce() once on widget mount instead. To pause the timer while a screen reader is active, check MediaQuery.of(context).accessibleNavigation or use the announcer service's isScreenReaderActive getter if available.

Ensure Timer is cancelled in dispose() to avoid calling setState on a disposed widget.

Testing Requirements

Write widget tests: verify the banner is removed from the widget tree after 5 pump-duration cycles matching the timer duration (use fake async). Verify the timer is cancelled when a tap is simulated via WidgetTester.tap and that the banner remains present after the original timeout period. Verify the auto-dismiss pauses when accessibility is active by mocking the announcer service. Write unit tests for the timer cancellation logic in isolation from the widget.

Manually test with VoiceOver (iOS simulator) and TalkBack (Android emulator): confirm the announcement is read aloud when the banner appears without focus shifting. Test boundary: set duration to 2 seconds and assert it clamps to 3 seconds; set to 60 seconds and assert it clamps to 30 seconds.

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.