critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

Foreground FCM messages trigger a custom in-app banner (OverlayEntry or flutter_local_notifications) showing title and body for 4 seconds; tapping the banner navigates via NotificationDeepLinkHandler
Background FCM messages are received by the Firebase background message handler (top-level function annotated @pragma('vm:entry-point')) and persisted to Supabase via NotificationRepository
Terminated-state messages are retrieved via FirebaseMessaging.instance.getInitialMessage() on app start and the deep link is applied post-initialisation
All received notifications (foreground, background, tap-opened) are persisted to the local NotificationRepository with correct timestamp, category, read status (false), and FCM message ID
Duplicate message IDs are detected and not persisted twice (idempotency check on FCM message_id)
Foreground banner does not appear for notification types that the user has disabled in NotificationPreferencesRepository
PushNotificationService exposes an unreadCount stream consumed by the notification badge widget
The background handler is a top-level Dart function (not a class method) to comply with Firebase isolate requirements
Integration test covers the full foreground message → banner → tap → navigation flow using mocked FirebaseMessaging and GoRouter
Service initialises without throwing if FCM token is not yet available (permission not granted)

Technical Requirements

frameworks
Flutter
Riverpod
firebase_messaging
flutter_local_notifications (for foreground banners on Android)
apis
FirebaseMessaging.onMessage stream (foreground)
FirebaseMessaging.onBackgroundMessage (background/terminated)
FirebaseMessaging.onMessageOpenedApp (background tap)
FirebaseMessaging.instance.getInitialMessage() (terminated tap)
Supabase insert via NotificationRepository
OverlayEntry or flutter_local_notifications for in-app banners
data models
NotificationRecord (id, fcm_message_id, user_id, title, body, category, deep_link_payload, is_read, received_at)
NotificationPreference
performance requirements
Background handler must complete within Firebase's 30-second window
Foreground banner must appear within 300ms of message receipt
unreadCount stream must update within 1 second of new message receipt
security requirements
Background handler must initialise its own Supabase client using stored credentials — it runs in a separate isolate with no shared state
Validate FCM message sender (from field matches expected Firebase project) before processing
Notification content must not be cached in plaintext on-device beyond the Supabase-backed NotificationRepository
RLS policy ensures a user can only read/write their own notification records
ui components
In-app foreground banner overlay (custom OverlayEntry)
Notification badge count on bottom nav tab

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Structure PushNotificationService as a Riverpod AsyncNotifier that subscribes to all three FirebaseMessaging streams in its build method. The background message handler must be a top-level async function (not a method) — place it in a dedicated notifications_background_handler.dart file. In main.dart, call FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler) before runApp(). For the foreground in-app banner, use an OverlayEntry inserted into the root overlay; create a custom BannerWidget that auto-dismisses after 4 seconds using a Timer.

For Android foreground notifications, configure flutter_local_notifications with a high-importance channel. Store a StreamController for unreadCount — increment on insert, recompute on mark-all-read. The background handler needs its own Supabase.initialize() call since it runs in a separate isolate — use flutter_secure_storage to retrieve credentials or hardcode the anon key (never service role).

Testing Requirements

Unit tests: mock FirebaseMessaging streams and assert NotificationRepository.insert() is called with correct payload for each lifecycle path. Test idempotency: calling handler twice with same message_id results in one DB record. Test preference filtering: foreground banner suppressed when category disabled. Test unreadCount stream emits correct value after insert and after mark-as-read.

Integration test: full foreground message flow using fake async and mocked overlay. Widget test: verify badge count updates when unreadCount stream emits. Test background handler function in isolation with mocked Supabase client.

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.