critical priority high complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

PushNotificationBloc is registered in the app's BlocProvider tree at root level and its initialization completes before the home screen is rendered
FirebaseMessaging.onMessage.listen() is registered in PushNotificationBloc._init() and emits a NotificationReceivedEvent to the BLoC stream for every foreground message
The top-level @pragma('vm:entry-point') backgroundHandler function is defined outside any class and registered via FirebaseMessaging.onBackgroundMessage() — it initializes Firebase before processing
getInitialMessage() is called once during init and, if non-null, emits a NotificationOpenedFromTerminatedEvent to the stream
FirebaseMessaging.onMessageOpenedApp.listen() emits a NotificationOpenedFromBackgroundEvent to the stream
FlutterLocalNotificationsPlugin is initialized with AndroidInitializationSettings('@mipmap/ic_launcher') and DarwinInitializationSettings() during BLoC init
The BLoC exposes a Stream<NotificationEvent> (via a StreamController) that downstream widgets and services can listen to
All stream subscriptions are cancelled and StreamController is closed in BLoC close() override to prevent memory leaks
BLoC correctly handles initialization failure (e.g., Firebase not initialized) by emitting a PushNotificationErrorState with a descriptive message
The initialization sequence is: Firebase init → FCMTokenManager.init() → FlutterLocalNotifications init → register handlers → emit PushNotificationInitializedState

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
firebase_messaging
flutter_local_notifications
Riverpod
apis
FirebaseMessaging.onMessage
FirebaseMessaging.onMessageOpenedApp
FirebaseMessaging.onBackgroundMessage
FirebaseMessaging.instance.getInitialMessage()
FlutterLocalNotificationsPlugin.initialize()
data models
NotificationEvent (sealed class: NotificationReceivedEvent, NotificationOpenedFromBackgroundEvent, NotificationOpenedFromTerminatedEvent)
PushNotificationState (sealed: PushNotificationInitial, PushNotificationInitializing, PushNotificationInitialized, PushNotificationErrorState)
performance requirements
BLoC initialization must complete within 2 seconds on app cold start
Background handler must be a pure top-level function with no UI dependencies — it runs in a separate isolate
security requirements
Background handler must reinitialize Firebase before any Supabase calls — do not assume Firebase is initialized in background isolate
Do not store full RemoteMessage objects in BLoC state — extract only needed fields to avoid holding large objects in memory

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The background handler MUST be a top-level function annotated with @pragma('vm:entry-point') — placing it inside a class will cause a runtime crash on Android. In the background handler, call Firebase.initializeApp() with the same DefaultFirebaseOptions before any other Firebase API. The BLoC should NOT directly call FCMTokenManager or NotificationDeepLinkHandler — wire these as injected dependencies via the constructor so they can be mocked in tests. Use a broadcast StreamController so multiple listeners (deep link handler, local notification display) can subscribe.

Keep the BLoC state machine minimal: its primary responsibility is lifecycle management and event routing, not business logic. Business logic belongs in FCMTokenManager and NotificationDeepLinkHandler.

Testing Requirements

Unit tests using flutter_test, bloc_test, and mocktail. Mock FirebaseMessaging and FlutterLocalNotificationsPlugin. Test cases: (1) init emits PushNotificationInitializing then PushNotificationInitialized, (2) onMessage fires — NotificationReceivedEvent emitted, (3) getInitialMessage returns message — NotificationOpenedFromTerminatedEvent emitted, (4) getInitialMessage returns null — no event emitted, (5) onMessageOpenedApp fires — NotificationOpenedFromBackgroundEvent emitted, (6) Firebase init throws — PushNotificationErrorState emitted, (7) BLoC close() cancels all subscriptions (verify via mock). Use bloc_test's emitsInOrder for state sequence validation.

Background handler cannot be unit tested directly — document this limitation.

Component
Push Notification Service
infrastructure medium
Epic Risks (4)
high impact high prob technical

Flutter's background message handler for FCM must run in a separate Dart isolate. Incorrect dependency initialization in the isolate (e.g., attempting to access Riverpod providers or Supabase before initialization) will cause silent crashes on Android when the app is terminated, resulting in missed notifications that are invisible in crash reporting.

Mitigation & Contingency

Mitigation: Use a minimal top-level background handler function annotated with @pragma('vm:entry-point') that only stores the raw RemoteMessage payload to a platform channel or shared preferences. Process the payload in the main isolate on next app launch. Write an explicit test for terminated-state message handling on Android.

Contingency: If isolate crashes are observed, implement a native Android FirebaseMessagingService subclass that handles background messages without Flutter isolate complexity, falling back to a database-insert-only approach for terminated-state notifications.

medium impact medium prob technical

Supabase Edge Functions can experience cold-start latency of 1–3 seconds after periods of inactivity. For high-frequency events like assignment creation, cumulative cold starts could cause dispatch delays exceeding the 30-second SLA, reducing the perceived reliability of the notification system.

Mitigation & Contingency

Mitigation: Configure the Edge Function with a keep-warm ping mechanism or use Supabase database webhooks that invoke the function directly on row insert to minimize cold-start frequency. Batch preference lookups within the function to reduce per-invocation Supabase round-trips.

Contingency: If latency SLA is consistently breached, move to a polling or Realtime-subscription architecture within the Edge Function, or pre-compute dispatch targets at preference-save time to eliminate per-dispatch preference queries.

high impact low prob security

If the deep link handler does not perform server-side role validation before rendering the target screen, a peer mentor who receives a mis-configured notification payload containing a coordinator-only route could access restricted data, violating the role-based access control invariants.

Mitigation & Contingency

Mitigation: The deep link handler must check the user's current role from the RoleStateManager before constructing the navigation route. Coordinator-only routes must be listed in a deny-list checked against the current role. The go_router route guard is a second line of defence.

Contingency: If a role bypass is discovered in testing, immediately add the affected route to the deep link handler deny-list and add a regression test. Audit all notification payload types for route targets that could expose cross-role data.

medium impact low prob dependency

FCM v1 HTTP API enforces per-project send quotas. For large organisations with many active peer mentors receiving simultaneous assignment notifications, batch dispatch events (e.g., bulk coordinator assignments) could approach quota limits and result in dropped notifications with 429 errors logged silently.

Mitigation & Contingency

Mitigation: Implement exponential backoff retry logic in the Edge Function for 429 responses. Design bulk assignment flows to dispatch notifications in batches with a configurable delay between batches. Monitor FCM console quotas during load testing.

Contingency: If quota limits are hit, implement a notification queue table in Supabase and a separate Edge Function that processes the queue with rate limiting, ensuring eventual delivery without exceeding FCM quotas.