nexo/docs/ios-notification-test-guide.md
toki 68ccfb13fd feat: update iOS notification test and plugin implementation
- Update RunnerTests.swift with notification test cases
- Update NexoMessagingPlugin.swift with notification handling
- Update ios-notification-test-guide.md
2026-06-01 13:38:32 +09:00

21 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 now registers the minimum debug/action bridge. packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift registers the legacy nexo_messaging method channel (for getPlatformVersion compatibility), the com.tokilabs.nexo.messaging/notification_actions method channel, and the com.tokilabs.nexo.messaging/notifications event channel.
  • The iOS bridge implements debugSendNativeEvent, saveDeviceToken, getDeviceToken, setAuthToken, clearAuthToken, setSigningKey, and getPlatformVersion. saveDeviceToken/getDeviceToken persist only the formatted token string via UserDefaults; setAuthToken, clearAuthToken, and setSigningKey are validation-only no-ops that do not persist auth tokens or signing secrets on iOS yet.
  • This is a debug/local-routing bridge only. APNs registration, Firebase token refresh handoff, notification center delegate, ACK, dismiss, and inline reply are not implemented and remain future smoke/implementation work.
  • 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.

The minimum iOS debug/action bridge (channel registration plus the method contract above) now exists, so Level 0 contract tests and local injection can exercise the Dart routing path. APNs registration, FCM token handoff, ACK, dismiss, inline reply, and native notification persistence are still follow-up implementation work.

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:

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 .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:

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 minimum debug/action bridge is in place, but the iOS native side still cannot complete a full APNs/FCM delivery smoke. Already done:

  • 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, with auth token/signing key kept as validation-only no-ops for now.

Remaining native bridge work:

  • 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.

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

{
  "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.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:

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

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