high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

On first call to requestPermission(), the iOS system dialog is shown to the user; the result (granted/denied/provisional) is captured and emitted on the permission stream
On Android 13+, the POST_NOTIFICATIONS runtime permission dialog is shown; on older Android versions, permission is assumed granted and emitted accordingly
The permissionStatus stream emits the current status synchronously on subscription (replay-last behaviour) so UI can render correct state without waiting
When the user navigates to OS settings and changes notification permission, the stream emits the updated status when the app returns to foreground (AppLifecycleState.resumed)
NotificationSettingsScreen correctly shows 'Enabled', 'Disabled', or 'Provisional' based on the stream value
If permission is denied, the manager exposes an openAppSettings() method that deep-links to OS notification settings; this is wired to a button in NotificationSettingsScreen
FCMTokenManager.activeToken stream only emits a non-null token after permission is granted; denied state prevents token registration
Unit tests cover all AuthorizationStatus enum values and the foreground-resume re-check logic

Technical Requirements

frameworks
Flutter
Riverpod
firebase_messaging
permission_handler (for openAppSettings)
apis
FirebaseMessaging.instance.requestPermission()
FirebaseMessaging.instance.getNotificationSettings()
AppLifecycleObserver (WidgetsBindingObserver)
openAppSettings() from permission_handler
data models
NotificationPermissionState (enum: granted, denied, provisional, notDetermined)
performance requirements
Permission status read must complete synchronously from cached value on stream subscribe
Foreground-resume re-check must not block UI thread — run asynchronously
security requirements
Never request permission without a clear user-facing explanation of why notifications are needed (comply with WCAG 2.2 AA and App Store guidelines)
Do not store permission state in Supabase — it is device-local OS state only
Respect user denial: do not re-prompt permission after denial; direct to settings instead
ui components
NotificationSettingsScreen permission status row
Open Settings button (AppButton)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as a Riverpod StateNotifierProvider or StreamProvider backed by a StreamController. In the constructor, immediately call FirebaseMessaging.instance.getNotificationSettings() and push the result to the stream. Mixin WidgetsBindingObserver on the notifier and override didChangeAppLifecycleState to re-check on resumed. Map FirebaseMessaging AuthorizationStatus to your internal enum to keep firebase_messaging out of UI layers.

Guard the FCMTokenManager token fetch: only call getToken() if permission state is granted or provisional. This ordering is enforced by making FCMTokenManager depend on NotificationPermissionManager in the Riverpod dependency graph.

Testing Requirements

Unit tests (flutter_test + mocktail): mock FirebaseMessaging.getNotificationSettings() to return each AuthorizationStatus variant and assert stream emits correct NotificationPermissionState. Test WidgetsBindingObserver resumed callback triggers a re-check. Widget test: render NotificationSettingsScreen with a mocked NotificationPermissionManager provider and assert correct status label renders for each state. Test openAppSettings() is called when the settings button is tapped while status is denied.

Component
Notification Settings Screen
ui low
Epic Risks (2)
medium impact medium prob technical

The notification badge widget depends on a persistent Supabase Realtime websocket subscription for live unread count updates. On mobile, network transitions (WiFi to cellular, background app state) can silently drop the websocket, resulting in a stale badge count that does not update until the next app foreground — reducing trust in the notification system.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management in the badge widget's BLoC that re-subscribes on app foreground and on network reconnection events. Add a fallback polling query (every 60 seconds when app is foregrounded) to reconcile the badge count if the Realtime subscription is interrupted.

Contingency: If Realtime reliability proves insufficient in production, replace the live subscription with a polling approach using a configurable interval, accepting slightly delayed badge updates in exchange for reliability.

medium impact medium prob technical

The notification list item widget requires merged semantics combining title, body, timestamp, read state, and role-context icon into a single VoiceOver/TalkBack announcement. Getting the merged semantics structure right for both iOS (VoiceOver) and Android (TalkBack) simultaneously is non-trivial and common to break silently when widgets are refactored.

Mitigation & Contingency

Mitigation: Use the project's existing semantics-wrapper-widget pattern with explicit Semantics widgets and excludeSemantics on decorative children. Write accessibility widget tests using Flutter's SemanticsController to assert the exact announcement string. Test on physical devices with VoiceOver and TalkBack enabled before release.

Contingency: If merged semantics cannot be achieved cleanly on both platforms, implement platform-specific semantic trees using defaultTargetPlatform branching, ensuring each platform receives an optimal announcement even if the implementation differs.