critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

getNotificationSettings() is called before requestPermission() to avoid re-requesting if already granted or denied
requestPermission() is only called when current status is notDetermined
FirebaseMessaging.requestPermission(alert: true, badge: true, sound: true) is called with exactly those parameters
Return value maps iOS AuthorizationStatus to PermissionResult enum: authorized → granted, denied → denied, provisional → provisional, notDetermined → notDetermined
PermissionResult is persisted to SharedPreferences under key 'notification_permission_result'
If result is 'granted', FCMTokenManager.registerTokenOnSignIn(userId) is called immediately
If result is 'denied', no FCM registration is attempted and no error is thrown
Unit test covers all four AuthorizationStatus mappings
Manual QA on iOS 16+ confirms the OS permission dialog appears on first launch and does not re-appear on subsequent launches when already granted

Technical Requirements

frameworks
Flutter
firebase_messaging ^15+
shared_preferences ^2+
apis
FirebaseMessaging.instance.getNotificationSettings()
FirebaseMessaging.instance.requestPermission()
SharedPreferences for result persistence
data models
PermissionResult (enum: granted, denied, provisional, notDetermined)
NotificationSettings (iOS AuthorizationStatus)
performance requirements
getNotificationSettings() call must complete within 1 second
Permission result persistence must be non-blocking (async write)
security requirements
No user identity data stored alongside permission result
Permission result key in SharedPreferences must not be used for security-gating features — it is a UX hint only
ui components
Pre-permission rationale dialog (implemented in task-011, called before this flow)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define PermissionResult as a Dart enum in lib/notifications/permission_result.dart — do not reuse any third-party enum to keep the domain model clean. In NotificationPermissionManager (lib/notifications/notification_permission_manager.dart), inject FirebaseMessaging and SharedPreferences via constructor for testability. The mapping from AuthorizationStatus to PermissionResult should be a pure switch expression with an exhaustive default arm. On iOS, 'provisional' authorization (quiet notifications) is a valid state — treat it as granted for token registration purposes but store it distinctly so the UI can optionally prompt the user to upgrade to full authorization later.

Do not call requestPermission() unless the pre-permission rationale dialog (task-011) has already been shown.

Testing Requirements

Unit tests (flutter_test + mockito): mock FirebaseMessaging.getNotificationSettings() returning each of the four AuthorizationStatus values; verify correct PermissionResult mapping; verify SharedPreferences write with correct key/value; verify registerTokenOnSignIn is called only when result is 'granted'. Widget test: render a test widget that calls requestPermission() and assert the returned enum value. Manual QA: fresh iOS simulator install → confirm dialog appears; second launch → confirm no dialog; Settings → disable notifications → reopen app → confirm 'denied' result stored and no crash.

Component
Notification Permission Manager
service low
Epic Risks (3)
high impact medium prob scope

iOS only allows one system permission prompt per app install. If the rationale dialog timing or content is wrong the user may permanently deny permissions during onboarding, permanently blocking push delivery for that device with no recovery path short of manual system settings navigation.

Mitigation & Contingency

Mitigation: Design and user-test the rationale dialog content and trigger point (after onboarding value-demonstration step, not at first launch). Implement the settings-deep-link fallback in NotificationPermissionManager so the permission state screen always offers a path to system settings if denied.

Contingency: If denial rates are high in TestFlight testing, revise the rationale copy and trigger timing before production release. Ensure the in-app notification centre provides full value without push so denied users are not blocked from the feature.

medium impact medium prob technical

FCM token rotation callbacks can fire at any time, including during app termination or network outage. If the token rotation is not persisted reliably the backend trigger service will dispatch to a stale token, resulting in silent notification failures that are hard to diagnose.

Mitigation & Contingency

Mitigation: Persist token rotation updates with a local queue that retries on next app foreground if network is unavailable. Use Supabase upsert by (user_id, device_id) to prevent duplicate token rows and ensure the latest token always wins.

Contingency: If token staleness is observed in production, add a token validity check on each app foreground and force a re-registration if the stored token does not match the FCM-reported current token.

high impact low prob security

Incorrect RLS policies on notification_preferences or fcm_tokens could expose one user's preferences or device tokens to another user, or could block the backend Edge Function service role from reading token lists needed for dispatch, silently dropping all notifications.

Mitigation & Contingency

Mitigation: Write explicit RLS policy tests using the Supabase test harness covering user-scoped read/write, service-role read for dispatch, and cross-user access denial. Review policies during code review with a security checklist.

Contingency: Maintain a rollback migration that reverts the RLS changes, and add an integration test in CI that asserts the service role can query all tokens and that a normal user JWT cannot access another user's token rows.