520 lines
19 KiB
Markdown
520 lines
19 KiB
Markdown
# 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
|
|
<key>aps-environment</key>
|
|
<string>development</string>
|
|
```
|
|
|
|
### 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": "<FCM_REGISTRATION_TOKEN>",
|
|
"notification": {
|
|
"title": "nexo smoke",
|
|
"body": "iOS notification smoke"
|
|
},
|
|
"data": {
|
|
"type": "message",
|
|
"server_url": "https://<SMOKE_SERVER>",
|
|
"channel_id": "<CHANNEL_ID>",
|
|
"root_id": "",
|
|
"is_crt_enabled": "false",
|
|
"ack_id": "<ACK_ID>",
|
|
"post_id": "<POST_ID>"
|
|
},
|
|
"apns": {
|
|
"headers": {
|
|
"apns-push-type": "alert",
|
|
"apns-priority": "10",
|
|
"apns-topic": "<BUNDLE_ID>"
|
|
},
|
|
"payload": {
|
|
"aps": {
|
|
"sound": "default"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Silent/background smoke payload shape:
|
|
|
|
```json
|
|
{
|
|
"message": {
|
|
"token": "<FCM_REGISTRATION_TOKEN>",
|
|
"data": {
|
|
"type": "message",
|
|
"server_url": "https://<SMOKE_SERVER>",
|
|
"channel_id": "<CHANNEL_ID>"
|
|
},
|
|
"apns": {
|
|
"headers": {
|
|
"apns-push-type": "background",
|
|
"apns-priority": "5",
|
|
"apns-topic": "<BUNDLE_ID>"
|
|
},
|
|
"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": "<BUNDLE_ID>",
|
|
"aps": {
|
|
"alert": {
|
|
"title": "nexo smoke",
|
|
"body": "local routing fixture"
|
|
},
|
|
"sound": "default"
|
|
},
|
|
"type": "message",
|
|
"server_url": "https://<SMOKE_SERVER>",
|
|
"channel_id": "<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 <IOS_DEVICE_ID>
|
|
```
|
|
|
|
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:<server>/<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:<server>/<root>`.
|
|
|
|
### 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
|