critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

On Android < 13 (API < 33), requestPermission() immediately returns PermissionResult.granted without showing any dialog
On Android 13+ (API 33+), permission_handler's Permission.notification.request() is called
PermissionStatus maps to PermissionResult: granted → granted, denied → denied, permanentlyDenied → denied (with a distinct flag or via a separate isPermanentlyDenied() helper)
shouldShowRationale() equivalent (permission_handler's PermissionStatus.isDenied with prior request recorded) is used to distinguish soft-denied from permanently-denied
PermissionResult is persisted to SharedPreferences under key 'notification_permission_result' (same key as iOS for consistency)
FCM token registration is triggered immediately on PermissionResult.granted
AndroidManifest.xml includes <uses-permission android:name='android.permission.POST_NOTIFICATIONS' /> declaration
Unit test confirms API-level branching: API < 33 path returns granted without calling permission_handler
Manual QA on Android 13 emulator confirms dialog appears; on Android 12 emulator confirms no dialog

Technical Requirements

frameworks
Flutter
permission_handler ^11+
apis
Permission.notification.status
Permission.notification.request()
DeviceInfoPlugin.androidInfo.version.sdkInt for API level detection
data models
PermissionResult (same enum as iOS task)
AndroidDeviceInfo.version.sdkInt
performance requirements
API level check must be synchronous or cached — do not call DeviceInfoPlugin on every permission check
Permission request must not block the main thread
security requirements
POST_NOTIFICATIONS permission must not be requested in a loop on denial — respect the OS rate limit
Permanently denied state must redirect to app settings, not silently re-request
ui components
Settings-redirect dialog for permanently denied state (implemented in task-011)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Cache the Android SDK version in a static field on first access to avoid repeated async calls to DeviceInfoPlugin. Use a version constant (kAndroid13ApiLevel = 33) in lib/notifications/notification_constants.dart rather than a magic number. In the NotificationPermissionManager, the Android and iOS flows should be dispatched via Platform.isAndroid / Platform.isIOS so the class is cross-platform. Permanently denied detection: after a failed request, call Permission.notification.status again — if it returns PermissionStatus.permanentlyDenied, set a SharedPreferences flag 'notification_permanently_denied': true and use openAppSettings() from permission_handler to redirect.

Never call Permission.notification.request() when permanently denied — it has no effect on Android 13+ and wastes a user interaction slot.

Testing Requirements

Unit tests (flutter_test + mockito): mock DeviceInfoPlugin returning sdkInt=32, verify no permission_handler call and granted result returned; mock sdkInt=33 with Permission.notification.request() returning PermissionStatus.granted, verify PermissionResult.granted and FCM registration triggered; mock returning PermissionStatus.permanentlyDenied, verify isPermanentlyDenied flag set. Integration test on real Android 13 device: run permission flow from cold start and confirm dialog appears. Manual regression: deny once (soft deny), re-run app, confirm rationale dialog shown; deny twice (permanent deny), confirm settings-redirect dialog shown.

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.