diff --git a/docs/README.md b/docs/README.md index f9378ee7..fa5c1b13 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ Module-specific instructions stay with the module that owns them. - Mattermost mobile app upstream snapshot baseline: `../apps/mattermost/UPSTREAM.md` - Mattermost push-proxy upstream snapshot baseline: `../services/push-proxy/UPSTREAM.md` - Runtime Image Validation details: `runtime-image-validation.md` +- iOS notification smoke prerequisites and runbook: `ios-notification-test-guide.md` ## Upstream Following Policy diff --git a/docs/ios-notification-test-guide.md b/docs/ios-notification-test-guide.md new file mode 100644 index 00000000..76854b55 --- /dev/null +++ b/docs/ios-notification-test-guide.md @@ -0,0 +1,520 @@ +# 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