high priority medium complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

NotificationDeepLinkHandler is a standalone service class (not a widget) injectable via Riverpod or passed via constructor
Handler accepts a notification payload object with fields: notification_type, entity_id, entity_type
Handler resolves all 6 entity types to their correct named routes: contact → /contacts/:id, peer_mentor → /peer-mentors/:id, activity → /activities/:id, certification → /certifications/:id, expense_claim → /expense-claims/:id, scenario → /scenarios/:id
Before navigating, the handler queries the corresponding repository using entity_id to verify existence
If repository returns a not-found result (null or 404-equivalent), navigation is aborted and a snackbar is shown with the message 'This item is no longer available'
On abort, the handler dispatches MarkNotificationRead to NotificationBLoC to prevent repeated dead-link taps
On successful navigation, the handler dispatches MarkNotificationRead to NotificationBLoC after navigation completes
Handler does not throw unhandled exceptions for any supported or unsupported entity type
Unknown/unsupported entity_type values are handled gracefully: log the unknown type and show a generic snackbar, do not crash
Handler can be invoked from both the notification list tap and a push notification tap (FCM/APNs)
Unit tests cover all 6 entity types for both success (entity exists → navigate) and failure (entity deleted → snackbar) paths

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
apis
Supabase (repository queries for each entity type)
data models
Contact
PeerMentor
Activity
Certification
ExpenseClaim
Scenario
NotificationPayload
performance requirements
Entity existence check must complete within 1 second on a 4G connection before navigation proceeds
Handler must not block the UI thread — all async operations must use async/await with proper Future handling
security requirements
Repository queries must use authenticated Supabase client — unauthenticated requests must not be made
entity_id must be validated as a non-empty string before querying to prevent empty-string Supabase queries
Handler must not expose entity data beyond the existence check — do not cache entity content in the handler
ui components
ScaffoldMessenger (Flutter built-in) for snackbar display
GoRouter or Navigator for route resolution

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Implement NotificationDeepLinkHandler as a class with a single public method `handle(NotificationPayload payload, BuildContext context)`. Use a switch statement (or map of handlers) keyed on entity_type to dispatch to entity-specific resolver methods. Each resolver follows the pattern: (1) call `repository.findById(entity_id)`, (2) if null navigate and mark read, else show snackbar and mark read. Register the handler as a Riverpod provider so it can be injected into both the notification list widget and the FCM foreground handler.

Use GoRouter's `context.push(route)` for navigation to preserve the navigation stack. Define all route constants in a centralized routes file to avoid string duplication. For the snackbar, use ScaffoldMessenger.of(context).showSnackBar() — ensure the context is valid at call time (check mounted if calling after async gap).

Testing Requirements

Write unit tests using flutter_test with mocked repositories for all 6 entity types. For each entity type test: (1) entity exists → correct route is pushed, MarkNotificationRead dispatched; (2) entity returns null/not-found → snackbar shown, MarkNotificationRead dispatched, no navigation. Also test: (3) unknown entity_type → graceful handling with no crash. Mock the NotificationBLoC and assert MarkNotificationRead event is dispatched in both success and failure paths.

Test that the handler is idempotent — calling it twice with the same notification_id does not navigate twice. Aim for 100% branch coverage across all entity type resolution paths.

Component
Notification Deep Link Handler
service medium
Epic Risks (3)
medium impact medium prob integration

If a referenced entity (contact, certification, activity) has been deleted or its RLS policy now excludes the current user, the deep link handler may navigate to a screen that renders in an error state or throws an unhandled exception.

Mitigation & Contingency

Mitigation: The deep link handler must perform a lightweight existence check (HEAD request or minimal SELECT) before pushing the route. Define a contract with each destination screen for how to handle a not-found entity ID passed as a route parameter.

Contingency: If the existence check itself fails (network error), navigate to the destination screen anyway and let it handle the error gracefully with its own error state; do not block navigation for network timeouts.

medium impact low prob technical

If the tab badge widget triggers a full rebuild of the bottom navigation bar on every unread count change, it will cause visible jank on devices with many active Realtime events (e.g., org admins receiving org-wide alerts).

Mitigation & Contingency

Mitigation: Scope the badge widget to a dedicated BlocSelector that rebuilds only when the unread count value changes, not on any BLoC state emission. Use RepaintBoundary to isolate the badge from the rest of the nav bar.

Contingency: If performance issues persist, debounce badge updates to a maximum of one rebuild per 500ms and display the last known count during the debounce window.

high impact medium prob scope

Complex swipe-to-mark-read gestures and dynamic list updates may conflict with VoiceOver/TalkBack navigation patterns, particularly for Blindeforbundet users who rely exclusively on screen readers.

Mitigation & Contingency

Mitigation: Provide a dedicated accessibility action (Semantics.onTap / CustomSemanticsAction) for mark-as-read on each list item so screen reader users do not need the swipe gesture. Test with VoiceOver on iOS and TalkBack on Android before each release.

Contingency: If the swipe gesture proves incompatible with assistive technologies, disable it when a screen reader is detected (via ScreenReaderDetectionService) and rely solely on the tap-to-read and accessible action pathways.