docs: iOS notification test guide added and README updated
This commit is contained in:
parent
b8ca657478
commit
a3d395e7c3
2 changed files with 521 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
520
docs/ios-notification-test-guide.md
Normal file
520
docs/ios-notification-test-guide.md
Normal 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
|
||||
Loading…
Reference in a new issue