high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

FCM payload data map containing a 'deep_link_type' key and optional 'entity_id' key is correctly parsed into a typed DeepLinkTarget sealed class
Supported deep link types: activity_detail, contact_profile, expense_claim, certification_screen, notification_center — any unrecognised type falls back to notification_center
Navigation is performed using GoRouter.of(context).go() or router.go() via a global navigator key, not push(), to prevent duplicate route stacking
When the app is in terminated state, the deep link is applied after app initialisation completes (checked via FirebaseMessaging.instance.getInitialMessage())
When the app is in background state, the deep link is applied immediately on tap (FirebaseMessaging.onMessageOpenedApp stream)
Foreground notification taps trigger deep link navigation without re-showing the banner
Deep link navigation to an entity that no longer exists (deleted activity, removed contact) shows a standard 'Not found' error screen without crashing
All deep link paths require the user to be authenticated; unauthenticated state redirects to login then resumes the deep link after sign-in
Unit tests cover payload parsing for all supported types and fallback behaviour for unknown types

Technical Requirements

frameworks
Flutter
GoRouter
Riverpod
firebase_messaging
apis
FirebaseMessaging.instance.getInitialMessage()
FirebaseMessaging.onMessageOpenedApp stream
GoRouter.of(context).go()
GlobalKey<NavigatorState> (for context-free navigation)
data models
DeepLinkTarget (sealed class: ActivityDetailTarget, ContactProfileTarget, ExpenseClaimTarget, CertificationTarget, NotificationCenterTarget)
RemoteMessage (FCM payload)
NotificationRecord
performance requirements
Deep link resolution and navigation must complete within 500ms of notification tap
Payload parsing must be synchronous — no async operations in the parse step
security requirements
Validate entity_id values are UUIDs before using in Supabase queries to prevent injection
Do not trust deep link targets from payload without checking user has permission to view the target entity (RLS enforced server-side)
Deep link parameters must not expose internal system IDs in logs

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a sealed class DeepLinkTarget with subclasses for each destination. Implement a static DeepLinkTarget.fromPayload(Map data) factory. In NotificationDeepLinkHandler, subscribe to FirebaseMessaging.onMessageOpenedApp in initState or a provider's build method. For terminated state, call getInitialMessage() once during app startup in main.dart and pass the result to the handler after the router is initialised.

Use a Riverpod provider to hold a pending DeepLinkTarget? that the router's redirect logic consumes and clears after navigation. Store a GlobalKey in a Riverpod provider or pass the GoRouter instance directly. Avoid navigating before the first frame is rendered — use WidgetsBinding.instance.addPostFrameCallback if needed for the terminated-state case.

Testing Requirements

Unit tests: test DeepLinkTarget.fromPayload() with valid payloads for all 5 types, empty map, missing keys, unknown type. Test that navigation is invoked with correct GoRouter path string for each target type. Integration test: simulate onMessageOpenedApp emission and assert router.go() called with expected route. Widget test: verify unauthenticated redirect logic routes to login.

Edge case tests: entity_id missing for a type that requires it falls back gracefully; non-UUID entity_id is sanitised.

Component
Notification Center Screen
ui medium
Epic Risks (2)
medium impact medium prob technical

The notification badge widget depends on a persistent Supabase Realtime websocket subscription for live unread count updates. On mobile, network transitions (WiFi to cellular, background app state) can silently drop the websocket, resulting in a stale badge count that does not update until the next app foreground — reducing trust in the notification system.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management in the badge widget's BLoC that re-subscribes on app foreground and on network reconnection events. Add a fallback polling query (every 60 seconds when app is foregrounded) to reconcile the badge count if the Realtime subscription is interrupted.

Contingency: If Realtime reliability proves insufficient in production, replace the live subscription with a polling approach using a configurable interval, accepting slightly delayed badge updates in exchange for reliability.

medium impact medium prob technical

The notification list item widget requires merged semantics combining title, body, timestamp, read state, and role-context icon into a single VoiceOver/TalkBack announcement. Getting the merged semantics structure right for both iOS (VoiceOver) and Android (TalkBack) simultaneously is non-trivial and common to break silently when widgets are refactored.

Mitigation & Contingency

Mitigation: Use the project's existing semantics-wrapper-widget pattern with explicit Semantics widgets and excludeSemantics on decorative children. Write accessibility widget tests using Flutter's SemanticsController to assert the exact announcement string. Test on physical devices with VoiceOver and TalkBack enabled before release.

Contingency: If merged semantics cannot be achieved cleanly on both platforms, implement platform-specific semantic trees using defaultTargetPlatform branching, ensuring each platform receives an optimal announcement even if the implementation differs.