- Add iOS notification preflight plan and code review (PLAN/CODE_REVIEW-cloud-G07) - Add iOS notification smoke evidence template - Update iOS notification test guide with preflight steps - Add Flutter test app iOS fixtures (APNS payloads, GoogleService config, entitlements) - Update README files for flutter-test, messaging_flutter, and docs
20 KiB
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.swiftcurrently registers anexo_messagingmethod channel and implements onlygetPlatformVersion. - The Dart plugin expects native notification and action channels:
com.tokilabs.nexo.messaging/notificationsandcom.tokilabs.nexo.messaging/notification_actions. apps/flutter-test/lib/main.dartalready callsFirebase.initializeApp()before starting the app.apps/flutter-test/ios/Runner/AppDelegate.swiftdoes not currently register for remote notifications or install aUNUserNotificationCenterdelegate.apps/flutter-test/ios/Runner/Info.plisthas no notification-specific keys.GoogleService-Info.plistis intentionally ignored by.gitignore.
The first meaningful iOS smoke target is therefore:
- Build and install the signed test app on a physical iOS device.
- Initialize Firebase successfully with a local
GoogleService-Info.plist. - Request notification permission.
- Obtain an APNs token and an FCM registration token.
- Deliver one visible FCM notification through APNs.
- Map a notification open into the Dart
NotificationOpenedEventcontract.
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-topicwhen 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: enableRemote notificationsif 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-environmentasdevelopmentfor debug smoke. - Provisioning profile includes the App ID and push entitlement.
Useful verification commands after building:
codesign -d --entitlements :- build/ios/iphoneos/Runner.app
Look for:
<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
.p8file 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
.p12if 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.plistis 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
Runnertarget resources. - The plist remains untracked because
.gitignoreignores**/GoogleService-Info.plist.
The test app already calls:
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, anddebugSendNativeEventfor 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().apnsTokenmanually. - 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.
iOS Token Contract
The current Dart implementation stores Android tokens as
android_rn-v2:<raw-fcm-token>. For iOS preflight, reserve the
Mattermost-compatible APNs server-registration format:
apple_rn-v2:<raw-apns-token>
Use this distinction during testing:
- APNs device token: server registration candidate for the existing Mattermost-compatible Apple push-proxy path.
- FCM registration token: Firebase direct-send smoke token. Do not register it with the nexo server unless a future server/push-proxy path explicitly supports FCM-backed iOS delivery.
- Direct APNs sends through Apple Push Notification Console also require the raw APNs token, not the FCM registration token.
Follow-up implementation should make token formatting platform-aware instead of using the current Android-oriented Dart constant.
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:
{
"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:
{
"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
debugSendNativeEventwhen 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:
{
"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"
}
Tracked fixture files live in apps/flutter-test/ios/Fixtures/:
open_channel.apns: channel routing payload.open_thread.apns: CRT thread routing payload.
Before simulator injection, replace REPLACE_WITH_IOS_BUNDLE_ID with the
signed Runner bundle identifier. These fixtures prove local payload shape and
open-routing parsing only; they do not prove APNs registration, FCM delivery, or
server ACK behavior.
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.plistexists 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-environmentis present in the built app entitlements. - Confirm no real credential values are staged in git.
2. Build And Install
From the Flutter test app:
cd apps/flutter-test
flutter pub get
flutter run -d <IOS_DEVICE_ID>
If Xcode signing configuration is not already set, open:
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 frompendingto 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_urlandchannel_id. - App UI updates
Last opened. - App UI updates
Last navigationwithchannel:<server>/<channel>.
For thread routing:
- Payload has
is_crt_enabled=true. - Payload has
server_urlandroot_id. - App UI updates
Last opened. - App UI updates
Last navigationwiththread:<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
UNTextInputNotificationActionand 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.
Use docs/ios-notification-smoke-evidence-template.md as the repeatable
evidence record when real device smoke begins.
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