Implement Notification Deep Link Handler route resolver
epic-in-app-notification-centre-ui-task-005 — Build the NotificationDeepLinkHandler service that parses a notification payload (notification_type, entity_id, entity_type) and resolves it to a named Flutter route. Supported entity types: contact, peer_mentor, activity, certification, expense_claim, scenario. For each entity type, query the relevant repository to confirm the entity still exists before navigating. If the entity has been deleted, display a user-friendly snackbar message and remain on the current screen. On successful navigation, dispatch a MarkNotificationRead event to the NotificationBLoC.
Acceptance Criteria
Technical Requirements
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.
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.
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.
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.