# iOS Notification Test Guide Last verified: 2026-06-01 This guide lists what is needed to run iOS notification smoke tests for `packages/messaging_flutter` through `apps/flutter-test`. It separates external Apple/Firebase setup, local host-app setup, current implementation gaps, and repeatable evidence to collect. Do not commit real Apple, APNs, Firebase, service-account, device-token, auth token, private endpoint, Team ID, or provisioning values into tracked docs. Use placeholders in examples and keep credential files in the approved local or secret-store location. ## Current Repository State - The iOS plugin is scaffold-only. `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift` currently registers a `nexo_messaging` method channel and implements only `getPlatformVersion`. - The Dart plugin expects native notification and action channels: `com.tokilabs.nexo.messaging/notifications` and `com.tokilabs.nexo.messaging/notification_actions`. - `apps/flutter-test/lib/main.dart` already calls `Firebase.initializeApp()` before starting the app. - `apps/flutter-test/ios/Runner/AppDelegate.swift` does not currently register for remote notifications or install a `UNUserNotificationCenter` delegate. - `apps/flutter-test/ios/Runner/Info.plist` has no notification-specific keys. - `GoogleService-Info.plist` is intentionally ignored by `.gitignore`. The first meaningful iOS smoke target is therefore: 1. Build and install the signed test app on a physical iOS device. 2. Initialize Firebase successfully with a local `GoogleService-Info.plist`. 3. Request notification permission. 4. Obtain an APNs token and an FCM registration token. 5. Deliver one visible FCM notification through APNs. 6. Map a notification open into the Dart `NotificationOpenedEvent` contract. ACK, dismiss, inline reply, and native notification persistence are follow-up implementation work unless the iOS native bridge is expanded first. ## Decisions Needed Before Testing Record the answers in a private run note or task evidence, not in tracked docs. | Decision | Why it matters | |---|---| | Apple Developer Team | Determines signing identity, App ID ownership, and provisioning profile. | | Bundle ID | Must match Xcode, Apple App ID, Firebase iOS app, and APNs topic. | | Provisioning flow | Automatic signing is easiest for smoke; manual profiles are better for controlled evidence. | | APNs auth style | Prefer token key `.p8` for Firebase/FCM; certificate `.p12` is a fallback if the project requires it. | | Firebase project/app | Existing project vs new smoke-only project affects data isolation and credential access. | | Firebase config storage | This repo ignores `GoogleService-Info.plist`; decide who provides it and where local copies live. | | Physical device | FCM via APNs requires a real iOS device for meaningful delivery testing. | | Evidence path | Decide where screenshots, logs, payloads, and run notes are stored. | | CI boundary | Keep real APNs/FCM delivery manual unless a signed device farm is explicitly available. | ## Apple Setup Checklist ### Account And App ID - Active Apple Developer Program membership. - Access to the target Apple Developer Team. - Bundle ID reserved for the iOS smoke app. - App ID with Push Notifications enabled. - Registered physical device if manual provisioning is used. The Bundle ID must be consistent everywhere: - Xcode `PRODUCT_BUNDLE_IDENTIFIER`. - Apple Developer App ID. - Firebase iOS app bundle ID. - APNs `apns-topic` when sending directly to APNs. ### Xcode Capabilities Open `apps/flutter-test/ios/Runner.xcworkspace` in Xcode and configure the `Runner` target. - `Signing & Capabilities > Team`: selected smoke Team. - `Signing & Capabilities > Bundle Identifier`: selected smoke Bundle ID. - `Signing & Capabilities > Push Notifications`: enabled. - `Signing & Capabilities > Background Modes`: enable `Remote notifications` if the smoke includes background/silent delivery or background FCM callbacks. Expected local file outcome: - A Runner entitlements file exists or Xcode build settings point to one. - The signed app includes `aps-environment` as `development` for debug smoke. - Provisioning profile includes the App ID and push entitlement. Useful verification commands after building: ```sh codesign -d --entitlements :- build/ios/iphoneos/Runner.app ``` Look for: ```xml aps-environment development ``` ### APNs Auth Key Or Certificate Preferred path for Firebase/FCM: - Create or reuse an APNs Authentication Key in Apple Developer. - Download the `.p8` file once. - Record the Key ID privately. - Record the Team ID privately. - Upload the `.p8`, Key ID, and Team ID to Firebase Cloud Messaging settings for the matching iOS app. Fallback path: - Create an APNs provider certificate for the App ID. - Export it as `.p12` if Firebase or a provider server requires certificate auth. - Track expiry and renewal ownership. Do not confuse these with iOS code-signing certificates. Code signing lets the app install and carry the push entitlement. APNs provider credentials let a server, Firebase, or Apple Push Notification Console send pushes. ## Firebase Setup Checklist ### Project And iOS App - Firebase project exists. - iOS app is registered with the exact smoke Bundle ID. - `GoogleService-Info.plist` is downloaded for that iOS app. - The plist is copied locally to `apps/flutter-test/ios/Runner/GoogleService-Info.plist`. - The plist is added to the Xcode `Runner` target resources. - The plist remains untracked because `.gitignore` ignores `**/GoogleService-Info.plist`. The test app already calls: ```dart WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); ``` If Firebase initialization fails on iOS, check that the plist is present in the Runner target and that the bundle ID in the plist matches the signed app. ### Cloud Messaging Settings In Firebase Console: - Open Project Settings. - Open Cloud Messaging. - Find the matching iOS app configuration. - Upload APNs auth key `.p8`, or configure APNs certificates if that is the selected path. - Confirm Firebase Cloud Messaging API is enabled for HTTP v1 sends. For server-side sends: - Use Firebase Admin SDK, or - Use FCM HTTP v1 with a service account or Application Default Credentials. Do not place service-account JSON in the repo. ## Native Bridge Work Needed For A Full Smoke The current iOS scaffold cannot yet complete the Dart notification contract. Minimum native bridge work: - Register the expected EventChannel: `com.tokilabs.nexo.messaging/notifications`. - Register the expected MethodChannel: `com.tokilabs.nexo.messaging/notification_actions`. - Implement `saveDeviceToken`, `getDeviceToken`, `setAuthToken`, `clearAuthToken`, `setSigningKey`, and `debugSendNativeEvent` for iOS or explicitly no-op them with clear smoke limitations. - Request user notification authorization or rely consistently on FlutterFire's `FirebaseMessaging.requestPermission`. - Call `UIApplication.shared.registerForRemoteNotifications()`. - Handle `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`. - Handle `application(_:didFailToRegisterForRemoteNotificationsWithError:)`. - Decide whether Firebase Messaging method swizzling stays enabled. - If swizzling is disabled, set `Messaging.messaging().apnsToken` manually. - Subscribe to FCM token refresh and forward token changes to Dart. - Set `UNUserNotificationCenter.current().delegate`. - Forward foreground receipt, notification tap, and launch-from-notification payloads into Dart. - Map APNs/FCM userInfo fields to `NotificationOpenedEvent`. Current Dart token formatting uses the Android-oriented prefix: ```dart const String _kDeviceTokenPrefix = 'android_rn'; ``` Before registering iOS tokens with a nexo server, define an iOS token prefix and server compatibility rule. A likely follow-up is to make the prefix platform-aware instead of hard-coded. ## Payload Contract For nexo routing, keep custom fields outside the APNs `aps` dictionary. FCM `data` values must be strings. Required or high-value fields: | Field | Purpose | |---|---| | `type` | `message`, `opened`, `clear`, `session`, or `token_refresh` equivalent in the Dart contract. | | `server_url` | Preferred direct routing target when available. | | `server_id` | Fallback key if the app resolves server IDs locally. | | `channel_id` | Channel routing. | | `root_id` | Thread routing when CRT is enabled. | | `is_crt_enabled` | Determines channel vs thread navigation behavior. | | `ack_id` | ACK contract and signed push validation. | | `post_id` | Notification identity and ACK enrichment. | Visible FCM HTTP v1 smoke payload shape: ```json { "message": { "token": "", "notification": { "title": "nexo smoke", "body": "iOS notification smoke" }, "data": { "type": "message", "server_url": "https://", "channel_id": "", "root_id": "", "is_crt_enabled": "false", "ack_id": "", "post_id": "" }, "apns": { "headers": { "apns-push-type": "alert", "apns-priority": "10", "apns-topic": "" }, "payload": { "aps": { "sound": "default" } } } } } ``` Silent/background smoke payload shape: ```json { "message": { "token": "", "data": { "type": "message", "server_url": "https://", "channel_id": "" }, "apns": { "headers": { "apns-push-type": "background", "apns-priority": "5", "apns-topic": "" }, "payload": { "aps": { "content-available": 1 } } } } } ``` Use visible alert smoke first. Silent pushes are best-effort and easier to misread during early setup. ## Test Levels ### Level 0: Repo Contract Tests No Apple/Firebase credentials required. - Dart widget tests. - Dart event mapping tests using `handleNativeEvent`. - Integration tests using `debugSendNativeEvent` when native debug bridge exists. This level validates the Dart routing contract only. It does not prove APNs or FCM delivery. ### Level 1: Simulator / Local Injection No real FCM via APNs. - Use simulator/local notification payload injection only to exercise userInfo parsing and open routing. - Do not mark FCM delivery, APNs token registration, or background delivery as passed from simulator evidence. Example local `.apns` fixture shape: ```json { "Simulator Target Bundle": "", "aps": { "alert": { "title": "nexo smoke", "body": "local routing fixture" }, "sound": "default" }, "type": "message", "server_url": "https://", "channel_id": "", "root_id": "", "is_crt_enabled": "false" } ``` ### Level 2: Physical Device FCM Smoke Required for meaningful iOS delivery. - Signed debug or release app installed on a real iPhone/iPad. - Notification permission requested and accepted. - APNs registration succeeds. - FCM registration token is displayed or logged. - A visible FCM message arrives. - Tapping the notification opens or resumes the app. - Dart receives a notification-open event with routing fields. ### Level 3: Nexo Server End-To-End Smoke Required before claiming server contract compatibility. - FCM token is registered with the nexo server using the agreed iOS token prefix. - Server can send a nexo-compatible push payload. - Signed payload validation behavior is known. - ACK request behavior is observed. - Open routing reaches the host app callback. ## Manual Smoke Runbook ### 1. Preflight - Confirm Xcode version and iOS device OS version. - Confirm Bundle ID in Xcode, Apple Developer, and Firebase match. - Confirm `GoogleService-Info.plist` exists locally and is part of Runner target resources. - Confirm APNs auth key or certificate is configured in Firebase. - Confirm the app target has Push Notifications capability. - Confirm `aps-environment` is present in the built app entitlements. - Confirm no real credential values are staged in git. ### 2. Build And Install From the Flutter test app: ```sh cd apps/flutter-test flutter pub get flutter run -d ``` If Xcode signing configuration is not already set, open: ```sh open ios/Runner.xcworkspace ``` Build from Xcode once to resolve signing and provisioning issues. ### 3. Permission And Token Expected evidence: - App launches without Firebase initialization error. - iOS notification permission prompt appears. - Permission state is logged. - APNs registration success is logged. - FCM token is logged or displayed in the app UI. - `Device token:` in the Flutter test app changes from `pending` to a formatted token once the Dart/native bridge supports iOS token handoff. Important distinction: - FCM sends to the FCM registration token returned by Firebase Messaging. - Apple Push Notification Console or direct APNs sends require the APNs device token from `didRegisterForRemoteNotificationsWithDeviceToken`. ### 4. Send A Visible FCM Message Use Firebase Admin SDK or FCM HTTP v1. Keep payload, response ID, timestamp, device state, and app logs as evidence. Expected result: - Foreground: app receives callback or foreground presentation according to the native delegate policy. - Background: system displays a notification. - Terminated: tapping the notification starts the app and exposes the launch payload. ### 5. Verify Routing For channel routing: - Payload has `is_crt_enabled=false`. - Payload has `server_url` and `channel_id`. - App UI updates `Last opened`. - App UI updates `Last navigation` with `channel:/`. For thread routing: - Payload has `is_crt_enabled=true`. - Payload has `server_url` and `root_id`. - App UI updates `Last opened`. - App UI updates `Last navigation` with `thread:/`. ### 6. Verify ACK, Dismiss, Inline Reply Boundaries Current expected state: - ACK is not passed unless an iOS ACK client/native bridge exists. - Dismiss is not passed unless notification category/delete handling exists. - Inline reply is not passed unless `UNTextInputNotificationAction` and reply forwarding exist. If these are not implemented, record them as known iOS gaps instead of smoke failures. ## Evidence To Keep Do keep: - Date, operator, device model, iOS version, Xcode version. - Bundle ID shape, but not private Team ID if policy treats it as sensitive. - Build configuration: Debug/Profile/Release. - Entitlement proof with secrets redacted. - Firebase project/app alias, not raw private IDs if policy forbids them. - Permission status. - APNs registration success/failure log. - FCM token presence, redacted. - FCM message ID or API response ID. - Payload shape with private values replaced by placeholders. - Screenshot or screen recording of notification display and app routing UI. - App logs around receipt, tap, routing, ACK/dismiss/reply if implemented. Do not keep: - `.p8`, `.p12`, provisioning profile, private key, or service-account JSON. - Full APNs device token or FCM registration token. - Full auth token, signing key, private server URL, or user data. ## Troubleshooting Matrix | Symptom | Likely cause | Check | |---|---|---| | Firebase initialization fails | Missing or wrong `GoogleService-Info.plist` | Confirm file is in Runner target and Bundle ID matches. | | Permission prompt never appears | Permission already decided, request not called, or initialization failed | Check iOS Settings and app logs. | | APNs registration fails | Missing entitlement or bad signing profile | Inspect built entitlements and provisioning profile. | | FCM token is null | APNs token not linked, Firebase app mismatch, network issue, or swizzling/delegate conflict | Check Firebase logs, APNs token callback, and Messaging setup. | | FCM send succeeds but no notification | Wrong token, wrong Firebase project, APNs key not uploaded, device offline, notification disabled | Confirm token source, Firebase app, APNs config, device notification settings. | | Direct APNs send fails | Using FCM token instead of APNs token, wrong topic, wrong environment | Use APNs device token and matching Bundle ID/environment. | | Foreground receives nothing | `UNUserNotificationCenter` delegate missing or not forwarding | Confirm delegate setup and callback implementation. | | Tap opens app but no routing | Launch/tap payload not forwarded to Dart | Check native open handler and EventChannel emission. | | Background/silent unreliable | iOS best-effort delivery, wrong headers, app force-quit, missing background mode | Start with visible alert smoke and test silent separately. | ## Completion Criteria For This Milestone Minimum pass for `ios-notification-test` planning smoke: - Apple/Firebase ownership and credential flow are decided. - Bundle ID and provisioning approach are documented privately. - Firebase iOS app and local plist path are confirmed. - Physical-device smoke path is confirmed. - Current native bridge gaps are listed. - A visible FCM/APNs smoke checklist exists with evidence fields. - CI/headless boundary is explicit. Minimum pass for a future implementation smoke: - iOS native bridge emits Dart notification events. - FCM token is obtained and handed to Dart with an iOS-compatible prefix. - Visible FCM notification arrives on a physical device. - Tapping the notification triggers `NotificationOpenedEvent`. - Channel and thread routing callbacks are exercised. - ACK/dismiss/inline reply are either implemented and verified or explicitly tracked as remaining gaps. ## Official References - Apple: Registering your app with APNs https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns - Apple: Generating a remote notification https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification - Apple: Sending notification requests to APNs https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns - Apple: Testing notifications using the Push Notification Console https://developer.apple.com/documentation/usernotifications/testing-notifications-using-the-push-notification-console - Firebase: Add Firebase to your Apple project https://firebase.google.com/docs/ios/setup - Firebase: Set up a Firebase Cloud Messaging client app on Apple platforms https://firebase.google.com/docs/cloud-messaging/get-started?platform=ios - Firebase: Receive messages in Apple platform apps https://firebase.google.com/docs/cloud-messaging/ios/receive-messages - Firebase: Build app server send requests https://firebase.google.com/docs/cloud-messaging/send-message - FlutterFire: FCM via APNs Integration https://firebase.flutter.dev/docs/messaging/apple-integration/ - FlutterFire: Cloud Messaging usage https://firebase.flutter.dev/docs/messaging/usage