Handle deleted entity gracefully in deep link navigation
epic-in-app-notification-centre-ui-task-006 — Extend the NotificationDeepLinkHandler to catch 404/not-found responses from repositories when resolving entity_id before navigation. Implement a fallback strategy: show an accessible error snackbar ('This item is no longer available'), mark the notification as read anyway to prevent repeated dead-link taps, and keep the user on their current screen. Add integration tests covering the deleted-entity path for each supported entity type. Ensure the handler does not throw unhandled exceptions that would crash the app.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Extend the entity resolver methods from task-005 with a uniform try/catch wrapper. Create a private method `_handleNavigationFailure(BuildContext context, String notificationId)` that (1) dispatches MarkNotificationRead, (2) shows the snackbar. Call this method from both the not-found branch and the catch block. This avoids duplicating the fallback logic across all 6 entity resolvers.
For the snackbar Semantics, wrap the SnackBar content in a Semantics widget with `liveRegion: true` so VoiceOver/TalkBack announces it automatically — critical for Blindeforbundet users. Define a single error string constant (e.g., `AppStrings.notificationEntityDeleted`) rather than inline strings. Log errors using the app's existing logger (e.g., `logger.warning('Deep link entity not found', {type: entity_type, id: entity_id})`).
Testing Requirements
Write integration tests (using flutter_test with a test Supabase environment or fully mocked repositories) covering the deleted-entity path for all 6 entity types. Each test: (1) set up repository mock to return null/throw NotFoundException, (2) invoke handler, (3) assert SnackBar with correct message appears in widget tree, (4) assert MarkNotificationRead event was dispatched. Additionally write tests for network failure (repository throws TimeoutException) and confirm same snackbar behavior.
Test that the app does not crash (no unhandled exception) for any entity type in the deleted path. Use WidgetTester.pump() to flush async operations before assertions.
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.