docs: iOS notification test guide added and README updated

This commit is contained in:
toki 2026-06-01 09:55:25 +09:00
parent b8ca657478
commit a3d395e7c3
2 changed files with 521 additions and 0 deletions

View file

@ -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

View file

@ -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
<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