critical priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

When PushNotificationBloc receives a NotificationReceivedEvent (foreground), it calls LocalNotificationDisplay.show(RemoteMessage) within 500ms
Android notification channel is created with id: 'peer_mentor_channel', name: 'Peer Mentor Notifications', importance: Importance.high, and enableVibration: true
iOS (Darwin) notification settings are configured with presentAlert: true, presentBadge: true, presentSound: true
The displayed notification title and body are mapped directly from RemoteMessage.notification?.title and .body; if either is null, sensible defaults from NotificationAccessibilityConfiguration are used
The full FCM payload data map is serialized to JSON and stored as the notification payload string for retrieval on tap
On notification tap (onDidReceiveNotificationResponse callback), the payload is deserialized and passed to NotificationDeepLinkHandler.handlePayload()
Accessibility: the notification's content title includes the semantic context (e.g., 'New assignment from [coordinator name]') per NotificationAccessibilityConfiguration labels
No duplicate local notification is shown if the same FCM message_id has already been displayed in the current app session (deduplicate by message_id in memory)
Unit tests cover: show called on foreground message, Android channel config correct, iOS config correct, tap triggers deep link handler, duplicate message_id suppressed

Technical Requirements

frameworks
Flutter
flutter_local_notifications
BLoC (flutter_bloc)
apis
FlutterLocalNotificationsPlugin.show()
FlutterLocalNotificationsPlugin.initialize() with onDidReceiveNotificationResponse
AndroidNotificationChannel
DarwinNotificationDetails
data models
LocalNotificationPayload (message_id, route_type, resource_id)
NotificationAccessibilityConfiguration (label strings)
performance requirements
Local notification must appear within 500ms of FCM message receipt
JSON serialization of payload must not block the main thread — use jsonEncode synchronously (it is fast enough for small payloads)
security requirements
Do not include sensitive personal data (names, diagnoses) in notification body — use generic labels like 'You have a new assignment'
Payload stored in notification must not contain auth tokens or session data
ui components
Android notification channel configuration
Local notification with title, body, and app icon

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Extract LocalNotificationDisplay as a separate injectable class (not nested in the BLoC) to keep the BLoC thin and testable. Use a local Set _shownMessageIds in LocalNotificationDisplay to deduplicate within the session — clear it on logout. The onDidReceiveNotificationResponse callback in FlutterLocalNotificationsPlugin.initialize() is a static/top-level function on some plugin versions; use the instance-level initialize overload with the callback parameter to avoid isolate issues. For the Android notification channel, create it eagerly during app startup (not lazily on first notification) so it exists before the first message arrives.

On iOS, foreground presentation options (presentAlert etc.) must also be set via FirebaseMessaging.instance.setForegroundNotificationPresentationOptions() for FCM-delivered notifications — flutter_local_notifications handles the local re-display case.

Testing Requirements

Widget/unit tests using flutter_test and mocktail. Mock FlutterLocalNotificationsPlugin and NotificationDeepLinkHandler. Test cases: (1) foreground RemoteMessage → show() called with correct title/body, (2) RemoteMessage with null notification → defaults used, (3) tap on notification → deep link handler receives deserialized payload, (4) same message_id received twice → show() called only once, (5) Android channel created with correct importance level, (6) iOS DarwinNotificationDetails has presentAlert true. Verify WCAG 2.2 AA: notification titles must not rely solely on color to convey meaning.

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.