diff --git a/agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md b/agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md index b1c70bfd..06f1218a 100644 --- a/agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md +++ b/agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md @@ -38,11 +38,11 @@ iOS token 계약, native bridge 최소 범위, placeholder, fixture, evidence Apple/Firebase/APNs 실자산이 없어도 먼저 확정할 수 있는 계약과 검증 표면을 분리한다. -- [ ] [token-contract] iOS device token prefix, FCM/APNs token 구분, nexo server 등록 호환 기준을 정리한다. +- [x] [token-contract] iOS device token prefix, FCM/APNs token 구분, nexo server 등록 호환 기준을 정리한다. - [ ] [bridge-scope] Dart가 기대하는 EventChannel/MethodChannel 이름, iOS 최소 구현 method, no-op/fallback 경계를 정리한다. -- [ ] [host-placeholder] entitlements, `GoogleService-Info.example.plist`, ignored credential 위치, Xcode target resource 기준을 정리한다. -- [ ] [fixtures] APNs/FCM payload fixture와 simulator/local injection으로 검증할 opened-routing 시나리오를 작성한다. -- [ ] [evidence-template] permission, APNs/FCM token, foreground/background/open routing, ACK/dismiss gap evidence 템플릿을 작성한다. +- [x] [host-placeholder] entitlements, `GoogleService-Info.example.plist`, ignored credential 위치, Xcode target resource 기준을 정리한다. +- [x] [fixtures] APNs/FCM payload fixture와 simulator/local injection으로 검증할 opened-routing 시나리오를 작성한다. +- [x] [evidence-template] permission, APNs/FCM token, foreground/background/open routing, ACK/dismiss gap evidence 템플릿을 작성한다. ## 완료 리뷰 @@ -66,6 +66,12 @@ Apple/Firebase/APNs 실자산이 없어도 먼저 확정할 수 있는 계약과 - 관련 경로: `packages/messaging_flutter/ios/`, `packages/messaging_flutter/lib/`, `apps/flutter-test/ios/`, `docs/ios-notification-test-guide.md` - 표준선(선택): 민감값 없이 placeholder, fixture, event 계약, evidence 양식을 먼저 고정한다 - 민감값 기준: Apple/Firebase/APNs credential, Team ID, profile, token, private host 값은 tracked 문서에 쓰지 않는다 +- 진행 근거: + - `packages/messaging_flutter/README.md`와 `docs/ios-notification-test-guide.md`에 iOS `apple_rn-v2:` server-registration 후보와 FCM/APNs token 구분을 기록했다. + - `apps/flutter-test/ios/Runner/GoogleService-Info.example.plist`와 `apps/flutter-test/ios/Runner/Runner.example.entitlements`를 tracked placeholder로 추가했다. + - `apps/flutter-test/ios/Fixtures/open_channel.apns`와 `apps/flutter-test/ios/Fixtures/open_thread.apns`에 local/simulator opened-routing fixture를 추가했다. + - `docs/ios-notification-smoke-evidence-template.md`에 permission, APNs/FCM token, open routing, ACK/dismiss/inline reply gap evidence 양식을 추가했다. + - `[bridge-scope]`는 구현 범위가 커서 `agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md`로 분리했다. - 선행 작업: 알림 파이프라인 고도화 - 후속 작업: iOS 알림 테스트 - 확인 필요: 없음 diff --git a/agent-task/m-ios-notification-preflight/CODE_REVIEW-cloud-G07.md b/agent-task/m-ios-notification-preflight/CODE_REVIEW-cloud-G07.md new file mode 100644 index 00000000..4b3ccac5 --- /dev/null +++ b/agent-task/m-ios-notification-preflight/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,171 @@ + + +# Code Review Reference - IOS_BRIDGE + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-01 +task=m-ios-notification-preflight, plan=0, tag=IOS_BRIDGE + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md` +- Task ids: + - `bridge-scope`: Dart가 기대하는 EventChannel/MethodChannel 이름, iOS 최소 구현 method, no-op/fallback 경계를 정리한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_0.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-ios-notification-preflight/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [IOS_BRIDGE-1] iOS action/event channel registration | [ ] | +| [IOS_BRIDGE-2] Minimum iOS method contract and no-op boundaries | [ ] | +| [IOS_BRIDGE-3] iOS bridge tests and docs alignment | [ ] | + +## 구현 체크리스트 + +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`에 Dart가 기대하는 action/event channel을 등록하고 기존 `getPlatformVersion` 호환 channel을 유지한다. +- [ ] iOS bridge의 최소 method set(`debugSendNativeEvent`, `saveDeviceToken`, `getDeviceToken`, `setAuthToken`, `clearAuthToken`, `setSigningKey`, `getPlatformVersion`)을 구현하고 secret 저장이 필요한 기능은 validation-only no-op 경계로 둔다. +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`에 iOS bridge unit tests를 추가하고 `docs/ios-notification-test-guide.md`의 bridge gap 문구를 구현 상태와 남은 범위에 맞게 갱신한다. +- [ ] remote Flutter/Xcode 검증과 deterministic structural checks를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-ios-notification-preflight/`를 `agent-task/archive/YYYY/MM/m-ios-notification-preflight/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-ios-notification-preflight/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Swift action/event channel names가 Dart constants와 정확히 일치하고 legacy `nexo_messaging` channel이 유지되는지 확인한다. +- `debugSendNativeEvent`가 EventChannel listener로 payload를 전달하고 invalid payload는 `INVALID_ARG`로 실패하는지 확인한다. +- `saveDeviceToken`/`getDeviceToken`은 formatted token string만 저장/반환하며, auth token/signing key는 이 plan에서 영구 저장하지 않는지 확인한다. +- docs는 minimum iOS bridge 구현 상태만 말하고 APNs/FCM delivery, ACK, dismiss, inline reply 완료를 과장하지 않는지 확인한다. +- 검증 결과는 실제 stdout/stderr이며 remote/Xcode 환경 차단이 있으면 근거와 재개 조건이 기록됐는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### IOS_BRIDGE-1 중간 검증 +```sh +$ rg --sort path -n "com\\.tokilabs\\.nexo\\.messaging/(notifications|notification_actions)|nexo_messaging" packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart +(output) +``` + +### IOS_BRIDGE-2 중간 검증 +```sh +$ git diff --check -- packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift +(output) +``` + +### IOS_BRIDGE-3 중간 검증 +```sh +$ git diff --check -- apps/flutter-test/ios/RunnerTests/RunnerTests.swift docs/ios-notification-test-guide.md +(output) +``` + +### 최종 검증 +```sh +$ git diff --check -- packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift apps/flutter-test/ios/RunnerTests/RunnerTests.swift docs/ios-notification-test-guide.md +(output) +``` + +```sh +$ ssh toki@toki-labs.com 'cd ~/works/nexo/packages/messaging_flutter && flutter pub get && flutter test && flutter analyze --no-fatal-infos' +(output) +``` + +```sh +$ ssh toki@toki-labs.com 'cd ~/works/nexo/apps/flutter-test && flutter pub get && flutter test && flutter test integration_test' +(output) +``` + +```sh +$ ssh toki@toki-labs.com 'command -v xcodebuild && cd ~/works/nexo/apps/flutter-test/ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination "platform=iOS Simulator,name=iPhone 15"' +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +Sections and their ownership: + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) | +| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS | +| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` -> `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` -> `[x]` only; final checkbox is mandatory before saving | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed; when filled, include exact decision, evidence, commands/output, why automatic follow-up cannot resolve it, and resume condition | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | +| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry | +| 코드리뷰 결과 | Review agent appends | Not included in stub | diff --git a/agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md b/agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md new file mode 100644 index 00000000..4292d8d4 --- /dev/null +++ b/agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md @@ -0,0 +1,363 @@ + + +# Plan - IOS_BRIDGE + +## 이 파일을 읽는 구현 에이전트에게 + +구현의 마지막 단계는 반드시 active `CODE_REVIEW-cloud-G07.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증 명령을 실행하고 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, 로그 아카이브, `complete.log`, task archive 이동은 code-review-skill 전용이다. + +구현 중 사용자만 결정할 수 있는 외부 환경, secret, scope 충돌이 발견되면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 사항, 근거, 실행한 명령/출력, 재개 조건을 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 만들지 않는다. + +## 배경 + +iOS Flutter plugin scaffold는 현재 template channel인 `nexo_messaging`과 `getPlatformVersion`만 갖고 있다. Dart runtime은 이미 `com.tokilabs.nexo.messaging/notifications` EventChannel과 `com.tokilabs.nexo.messaging/notification_actions` MethodChannel을 호출하므로 iOS에서 debug/local routing smoke와 host integration이 막힌다. Apple Developer, APNs, Firebase 실자산 준비 전에도 native bridge 최소 계약과 no-op 경계를 먼저 고정한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 사용자 결정, 사용자 소유 외부 환경/secret/service 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션을 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 채운다. code-review가 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md` +- Task ids: + - `bridge-scope`: Dart가 기대하는 EventChannel/MethodChannel 이름, iOS 최소 구현 method, no-op/fallback 경계를 정리한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/messaging-runtime/PHASE.md` +- `agent-roadmap/phase/messaging-runtime/milestones/ios-notification-preflight.md` +- `agent-test/local/rules.md` +- `agent-test/local/messaging-flutter-smoke.md` +- `agent-test/local/client-app-smoke.md` +- `agent-test/local/workspace-ops-smoke.md` +- `agent-ops/rules/project/domain/messaging-flutter/rules.md` +- `agent-ops/rules/project/domain/client-app/rules.md` +- `agent-ops/rules/project/domain/workspace-ops/rules.md` +- `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift` +- `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart` +- `packages/messaging_flutter/test/nexo_messaging_test.dart` +- `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt` +- `packages/messaging_flutter/pubspec.yaml` +- `apps/flutter-test/ios/Runner/AppDelegate.swift` +- `apps/flutter-test/ios/RunnerTests/RunnerTests.swift` +- `apps/flutter-test/integration_test/plugin_integration_test.dart` +- `apps/flutter-test/lib/main.dart` +- `apps/flutter-test/pubspec.yaml` +- `apps/flutter-test/README.md` +- `docs/ios-notification-test-guide.md` +- `.gitignore` + +### 테스트 환경 규칙 + +`test_env=local`이다. `agent-test/local/rules.md`는 존재하고 읽었으며, 현재 환경에서 테스트 실행은 금지하고 기본 로컬 검증은 원격 `ssh toki@toki-labs.com`에서 수행하도록 한다. 변경 범위가 `packages/messaging_flutter/**`, `apps/flutter-test/**`, task artifact/docs에 걸치므로 `messaging-flutter-smoke`, `client-app-smoke`, `workspace-ops-smoke`를 읽었다. + +적용 명령은 remote `flutter pub get`, `flutter test`, `flutter analyze --no-fatal-infos`, `flutter test integration_test`이다. agent-test에는 iOS Swift/Xcode native unit command가 없으므로 `apps/flutter-test/ios/Runner.xcworkspace`와 `RunnerTests.swift`를 근거로 `xcodebuild test`를 fallback 검증으로 둔다. `<확인 필요>` 값은 없지만, Xcode/iOS Simulator가 원격 host에 없으면 구현 에이전트가 `command -v xcodebuild` 출력과 함께 사용자 리뷰 요청 또는 검증 차단을 기록해야 한다. + +### 테스트 커버리지 공백 + +- iOS native channel registration: 기존 Dart 테스트는 channel constant만 확인하며 iOS `FlutterEventChannel`/`FlutterMethodChannel` 등록은 검증하지 않는다. +- iOS native method handling: 기존 iOS `RunnerTests.swift`는 `getPlatformVersion`만 검증한다. +- `debugSendNativeEvent` event delivery: Dart integration test는 method call을 사용하지만 iOS native가 아직 action/event channel을 구현하지 않아 iOS에서 실패한다. +- APNs/FCM permission, token registration, foreground/background delivery, ACK, dismiss, inline reply는 이 plan 범위 밖이며 기존 자동 테스트도 없다. + +### 심볼 참조 + +이 plan은 symbol rename/remove를 하지 않는다. 새로 iOS native에서 구현해야 하는 기존 Dart/native 계약 참조는 다음과 같다. + +- Channel constants: `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:10-13`, `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt:27-30`, `apps/flutter-test/integration_test/plugin_integration_test.dart:10-20`. +- Method calls: Dart `debugSendNativeEvent`, `saveDeviceToken`, `getDeviceToken`, `setAuthToken`, `clearAuthToken`, `setSigningKey` call sites는 `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:78-224`에 있다. +- Android reference implementation: `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt:173-239`. + +### 분할 판단 + +split decision policy를 plan 파일 선택 전에 평가했다. 단일 plan을 선택한다. + +- ownership boundary는 `packages/messaging_flutter`의 iOS native bridge와 그 host test 하나로 모인다. +- Dart public API와 call-site는 이미 존재하며 rename/remove가 없어서 API/foundation과 rollout을 나눌 필요가 없다. +- APNs/Firebase 실기기 smoke, AppDelegate remote-notification lifecycle, ACK/dismiss/inline reply 구현은 제외했으므로 device-dependent evidence recovery를 별도 subtask로 분리하지 않는다. +- Swift implementation과 XCTest는 함께 리뷰해야 iOS bridge 계약 누락을 바로 잡을 수 있어 단일 plan 조정 비용이 더 낮다. + +### 범위 결정 근거 + +이 plan은 iOS minimum bridge contract만 구현한다. 실제 Apple Developer 설정, APNs key/certificate, provisioning profile, Firebase iOS app 생성, `GoogleService-Info.plist` 실파일 추가, AppDelegate remote notification registration, `UNUserNotificationCenter` delegate, real APNs/FCM delivery smoke는 후속 `iOS 알림 테스트` 범위다. Android native behavior, server/push-proxy token registration, ACK/dismiss/inline reply network delivery도 변경하지 않는다. + +### 빌드 등급 + +`cloud-G07`: iOS native Swift bridge와 Flutter platform channel boundary를 건드리고, 현재 환경에서 테스트가 금지되어 remote/Xcode 검증 의존성이 있다. + +## 구현 체크리스트 + +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`에 Dart가 기대하는 action/event channel을 등록하고 기존 `getPlatformVersion` 호환 channel을 유지한다. +- [ ] iOS bridge의 최소 method set(`debugSendNativeEvent`, `saveDeviceToken`, `getDeviceToken`, `setAuthToken`, `clearAuthToken`, `setSigningKey`, `getPlatformVersion`)을 구현하고 secret 저장이 필요한 기능은 validation-only no-op 경계로 둔다. +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`에 iOS bridge unit tests를 추가하고 `docs/ios-notification-test-guide.md`의 bridge gap 문구를 구현 상태와 남은 범위에 맞게 갱신한다. +- [ ] remote Flutter/Xcode 검증과 deterministic structural checks를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [IOS_BRIDGE-1] iOS action/event channel registration + +#### 문제 + +`packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift:4-9`는 template `nexo_messaging` MethodChannel만 등록한다. + +```swift +4 public class NexoMessagingPlugin: NSObject, FlutterPlugin { +5 public static func register(with registrar: FlutterPluginRegistrar) { +6 let channel = FlutterMethodChannel(name: "nexo_messaging", binaryMessenger: registrar.messenger()) +7 let instance = NexoMessagingPlugin() +8 registrar.addMethodCallDelegate(instance, channel: channel) +9 } +``` + +Dart는 `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:10-13`의 `com.tokilabs.nexo.messaging/notifications`와 `com.tokilabs.nexo.messaging/notification_actions`를 사용한다. + +#### 해결 방법 + +`NexoMessagingPlugin`을 `FlutterStreamHandler`도 구현하도록 확장하고, registration 시 action channel과 event channel을 만든다. 기존 template channel은 `getPlatformVersion` 테스트/호환을 위해 method delegate로 유지한다. + +```swift +private enum NexoMessagingChannels { + static let legacy = "nexo_messaging" + static let events = "com.tokilabs.nexo.messaging/notifications" + static let actions = "com.tokilabs.nexo.messaging/notification_actions" +} + +public class NexoMessagingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { + private var eventSink: FlutterEventSink? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = NexoMessagingPlugin() + let messenger = registrar.messenger() + + let legacyChannel = FlutterMethodChannel( + name: NexoMessagingChannels.legacy, + binaryMessenger: messenger + ) + registrar.addMethodCallDelegate(instance, channel: legacyChannel) + + let actionChannel = FlutterMethodChannel( + name: NexoMessagingChannels.actions, + binaryMessenger: messenger + ) + registrar.addMethodCallDelegate(instance, channel: actionChannel) + + let eventChannel = FlutterEventChannel( + name: NexoMessagingChannels.events, + binaryMessenger: messenger + ) + eventChannel.setStreamHandler(instance) + } +} +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: channel constants, action channel, event channel, legacy channel registration을 추가한다. +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: `onListen`/`onCancel`에서 `eventSink`를 설정/해제한다. + +#### 테스트 작성 + +직접 XCTest는 `IOS_BRIDGE-3`에서 작성한다. 이 항목은 registration 구조 변경이며 중간 검증으로 Swift/Dart channel constant가 문서화된 값과 일치하는지 확인한다. + +#### 중간 검증 + +```sh +rg --sort path -n "com\\.tokilabs\\.nexo\\.messaging/(notifications|notification_actions)|nexo_messaging" packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart +``` + +기대 결과: Swift와 Dart 모두 동일한 event/action channel name을 포함하고, Swift는 legacy `nexo_messaging`도 유지한다. + +### [IOS_BRIDGE-2] Minimum iOS method contract and no-op boundaries + +#### 문제 + +`packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift:11-18`는 `getPlatformVersion` 외 모든 method를 `FlutterMethodNotImplemented`로 반환한다. + +```swift +11 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { +12 switch call.method { +13 case "getPlatformVersion": +14 result("iOS " + UIDevice.current.systemVersion) +15 default: +16 result(FlutterMethodNotImplemented) +17 } +18 } +``` + +Dart는 `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:78-224`에서 action channel로 `debugSendNativeEvent`, `saveDeviceToken`, `getDeviceToken`, `setAuthToken`, `clearAuthToken`, `setSigningKey`를 호출한다. + +#### 해결 방법 + +Android reference와 같은 method names/argument validation을 iOS에 추가한다. APNs/Firebase/ACK 구현이 필요한 기능은 성공 no-op 또는 minimal local storage로 범위를 제한한다. + +```swift +private enum NexoMessagingKeys { + static let deviceToken = "nexo.messaging.deviceToken" +} + +public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "debugSendNativeEvent": + guard let payload = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARG", message: "event payload is null", details: nil)) + return + } + eventSink?(payload) + result(nil) + case "saveDeviceToken": + guard let token = argument("token", from: call) else { + result(FlutterError(code: "INVALID_ARG", message: "token is null", details: nil)) + return + } + UserDefaults.standard.set(token, forKey: NexoMessagingKeys.deviceToken) + result(nil) + case "getDeviceToken": + result(UserDefaults.standard.string(forKey: NexoMessagingKeys.deviceToken)) + case "setAuthToken": + guard argument("serverUrl", from: call) != nil, argument("token", from: call) != nil else { + result(FlutterError(code: "INVALID_ARG", message: "serverUrl or token is null", details: nil)) + return + } + result(nil) + case "setSigningKey": + guard argument("serverUrl", from: call) != nil, argument("signingKey", from: call) != nil else { + result(FlutterError(code: "INVALID_ARG", message: "serverUrl or signingKey is null", details: nil)) + return + } + result(nil) + case "clearAuthToken": + guard argument("serverUrl", from: call) != nil else { + result(FlutterError(code: "INVALID_ARG", message: "serverUrl is null", details: nil)) + return + } + result(nil) + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } +} +``` + +Use a small helper such as `private func argument(_ name: String, from call: FlutterMethodCall) -> String?` that reads `call.arguments as? [String: Any]`. + +#### 수정 파일 및 체크리스트 + +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: method switch에 minimum method set을 추가한다. +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: invalid argument는 Android와 같은 `INVALID_ARG` code로 반환한다. +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: `saveDeviceToken`/`getDeviceToken`은 formatted token string만 저장/반환한다. +- [ ] `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift`: `setAuthToken`, `clearAuthToken`, `setSigningKey`는 validation-only no-op으로 두고 token/signing secret을 영구 저장하지 않는다. + +#### 테스트 작성 + +`IOS_BRIDGE-3`에서 XCTest를 추가한다. 별도 Dart 테스트는 작성하지 않는다. Dart channel constants와 method-call behavior는 `packages/messaging_flutter/test/nexo_messaging_test.dart`가 이미 검증하며, 이 항목은 iOS native side 구현이다. + +#### 중간 검증 + +```sh +git diff --check -- packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift +``` + +기대 결과: whitespace error가 없다. + +### [IOS_BRIDGE-3] iOS bridge tests and docs alignment + +#### 문제 + +`apps/flutter-test/ios/RunnerTests/RunnerTests.swift:14-25`는 `getPlatformVersion`만 테스트한다. + +```swift +14 func testGetPlatformVersion() { +15 let plugin = NexoMessagingPlugin() +16 +17 let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) +18 +19 let resultExpectation = expectation(description: "result block must be called.") +20 plugin.handle(call) { result in +21 XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) +22 resultExpectation.fulfill() +23 } +24 waitForExpectations(timeout: 1) +25 } +``` + +`docs/ios-notification-test-guide.md` still describes the iOS bridge as a follow-up gap, so implementation completion must update the guide without claiming APNs/FCM delivery is done. + +#### 해결 방법 + +Add focused XCTest coverage for the new method contract and stream handler. Keep tests deterministic and credential-free. + +```swift +func testDebugSendNativeEventForwardsPayloadToEventSink() { + let plugin = NexoMessagingPlugin() + var forwarded: Any? + + _ = plugin.onListen(withArguments: nil, eventSink: { event in + forwarded = event + }) + + plugin.handle(FlutterMethodCall(methodName: "debugSendNativeEvent", arguments: [ + "type": "opened", + "channel_id": "channel-ios" + ])) { result in + XCTAssertNil(result) + } + + XCTAssertEqual((forwarded as? [String: Any])?["channel_id"] as? String, "channel-ios") +} +``` + +Also test `saveDeviceToken`/`getDeviceToken`, invalid argument errors for required args, and success no-op for auth/signing methods. Update `docs/ios-notification-test-guide.md` so the bridge section says the minimum iOS debug/action bridge exists while APNs registration, Firebase token refresh, notification center delegate, ACK, dismiss, and inline reply remain future smoke/implementation work. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`: debug event sink forwarding test를 추가한다. +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`: device token save/get test를 추가하고 test key cleanup을 수행한다. +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`: required argument invalid cases가 `FlutterError.code == "INVALID_ARG"`를 반환하는지 검증한다. +- [ ] `apps/flutter-test/ios/RunnerTests/RunnerTests.swift`: auth/signing no-op success boundaries를 검증한다. +- [ ] `docs/ios-notification-test-guide.md`: minimum iOS bridge 구현 상태와 남은 APNs/FCM 범위를 분리해 갱신한다. + +#### 테스트 작성 + +XCTest를 작성한다. Dart unit/integration tests는 기존 tests를 실행해 regression을 확인하되, 새 Dart test는 추가하지 않는다. + +#### 중간 검증 + +```sh +git diff --check -- apps/flutter-test/ios/RunnerTests/RunnerTests.swift docs/ios-notification-test-guide.md +``` + +기대 결과: whitespace error가 없다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift` | `IOS_BRIDGE-1`, `IOS_BRIDGE-2` | +| `apps/flutter-test/ios/RunnerTests/RunnerTests.swift` | `IOS_BRIDGE-3` | +| `docs/ios-notification-test-guide.md` | `IOS_BRIDGE-3` | + +## 최종 검증 + +```sh +git diff --check -- packages/messaging_flutter/ios/Classes/NexoMessagingPlugin.swift apps/flutter-test/ios/RunnerTests/RunnerTests.swift docs/ios-notification-test-guide.md +``` + +```sh +ssh toki@toki-labs.com 'cd ~/works/nexo/packages/messaging_flutter && flutter pub get && flutter test && flutter analyze --no-fatal-infos' +``` + +```sh +ssh toki@toki-labs.com 'cd ~/works/nexo/apps/flutter-test && flutter pub get && flutter test && flutter test integration_test' +``` + +```sh +ssh toki@toki-labs.com 'command -v xcodebuild && cd ~/works/nexo/apps/flutter-test/ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination "platform=iOS Simulator,name=iPhone 15"' +``` + +기대 결과: 모든 명령 exit code `0`. 마지막 명령에서 `xcodebuild` 또는 지정 simulator가 없으면 임의 대체하지 말고 실제 출력과 함께 `계획 대비 변경 사항` 또는 `사용자 리뷰 요청`에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/flutter-test/README.md b/apps/flutter-test/README.md index f88bf0c0..41491b96 100644 --- a/apps/flutter-test/README.md +++ b/apps/flutter-test/README.md @@ -74,6 +74,17 @@ Use the plugin-owned [`Manual Smoke Checklist`](../../packages/messaging_flutter/README.md#manual-smoke-checklist) for FCM delivery, ACK, inline reply, dismiss, and routing evidence. +For iOS preflight and smoke preparation, keep real Firebase and Apple files out +of git. Copy `ios/Runner/GoogleService-Info.example.plist` to +`ios/Runner/GoogleService-Info.plist` locally, replace placeholder values, and +make sure the real plist is added to the Runner target resources in Xcode. +Use `ios/Runner/Runner.example.entitlements` as the minimal Push Notifications +entitlement reference, then configure the real Runner target through Xcode. +Local routing fixtures live under `ios/Fixtures/*.apns`; replace +`REPLACE_WITH_IOS_BUNDLE_ID` before simulator injection. +See [`../../docs/ios-notification-test-guide.md`](../../docs/ios-notification-test-guide.md) +and [`../../docs/ios-notification-smoke-evidence-template.md`](../../docs/ios-notification-smoke-evidence-template.md). + ## Verification The integration test uses the plugin's test/debug event injection path for diff --git a/apps/flutter-test/ios/Fixtures/open_channel.apns b/apps/flutter-test/ios/Fixtures/open_channel.apns new file mode 100644 index 00000000..08ccae00 --- /dev/null +++ b/apps/flutter-test/ios/Fixtures/open_channel.apns @@ -0,0 +1,19 @@ +{ + "Simulator Target Bundle": "REPLACE_WITH_IOS_BUNDLE_ID", + "aps": { + "alert": { + "title": "nexo channel smoke", + "body": "open channel routing fixture" + }, + "sound": "default", + "category": "NEXO_MESSAGE" + }, + "type": "message", + "server_url": "https://example.invalid", + "server_id": "server-smoke", + "channel_id": "channel-smoke", + "root_id": "", + "is_crt_enabled": "false", + "ack_id": "ack-smoke-channel", + "post_id": "post-smoke-channel" +} diff --git a/apps/flutter-test/ios/Fixtures/open_thread.apns b/apps/flutter-test/ios/Fixtures/open_thread.apns new file mode 100644 index 00000000..48b764d7 --- /dev/null +++ b/apps/flutter-test/ios/Fixtures/open_thread.apns @@ -0,0 +1,19 @@ +{ + "Simulator Target Bundle": "REPLACE_WITH_IOS_BUNDLE_ID", + "aps": { + "alert": { + "title": "nexo thread smoke", + "body": "open CRT thread routing fixture" + }, + "sound": "default", + "category": "NEXO_MESSAGE" + }, + "type": "message", + "server_url": "https://example.invalid", + "server_id": "server-smoke", + "channel_id": "channel-smoke", + "root_id": "thread-smoke", + "is_crt_enabled": "true", + "ack_id": "ack-smoke-thread", + "post_id": "post-smoke-thread" +} diff --git a/apps/flutter-test/ios/Runner/GoogleService-Info.example.plist b/apps/flutter-test/ios/Runner/GoogleService-Info.example.plist new file mode 100644 index 00000000..6a743951 --- /dev/null +++ b/apps/flutter-test/ios/Runner/GoogleService-Info.example.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + REPLACE_WITH_FIREBASE_IOS_CLIENT_ID + REVERSED_CLIENT_ID + REPLACE_WITH_REVERSED_CLIENT_ID + API_KEY + REPLACE_WITH_FIREBASE_API_KEY + GCM_SENDER_ID + REPLACE_WITH_SENDER_ID + PLIST_VERSION + 1 + BUNDLE_ID + REPLACE_WITH_IOS_BUNDLE_ID + PROJECT_ID + REPLACE_WITH_FIREBASE_PROJECT_ID + STORAGE_BUCKET + REPLACE_WITH_FIREBASE_STORAGE_BUCKET + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + REPLACE_WITH_FIREBASE_GOOGLE_APP_ID + + diff --git a/apps/flutter-test/ios/Runner/Runner.example.entitlements b/apps/flutter-test/ios/Runner/Runner.example.entitlements new file mode 100644 index 00000000..903def2a --- /dev/null +++ b/apps/flutter-test/ios/Runner/Runner.example.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/docs/README.md b/docs/README.md index fa5c1b13..f1283144 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Module-specific instructions stay with the module that owns them. - 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` +- iOS notification smoke evidence template: `ios-notification-smoke-evidence-template.md` ## Upstream Following Policy diff --git a/docs/ios-notification-smoke-evidence-template.md b/docs/ios-notification-smoke-evidence-template.md new file mode 100644 index 00000000..6f353b47 --- /dev/null +++ b/docs/ios-notification-smoke-evidence-template.md @@ -0,0 +1,96 @@ +# iOS Notification Smoke Evidence Template + +Use this template when collecting iOS notification smoke evidence. Replace every +private value with a placeholder before storing output in tracked files. + +## Run Metadata + +- Date: +- Operator: +- Device model: +- iOS version: +- Xcode version: +- Flutter version: +- Build mode: Debug | Profile | Release +- Bundle ID: `` +- Firebase project/app alias: `` +- APNs auth path: key | certificate | Firebase-managed | direct APNs console + +## Preflight Inputs + +- Local `GoogleService-Info.plist` present in `apps/flutter-test/ios/Runner/`: yes | no +- Example plist matched: yes | no +- Entitlements include `aps-environment`: yes | no +- Push Notifications capability enabled: yes | no +- Background Modes > Remote notifications enabled: yes | no | not tested +- iOS token contract used: `apple_rn-v2:` +- FCM smoke token used only for Firebase direct send: yes | no | not tested + +## Permission And Token Evidence + +Paste sanitized logs only. + +```text +permission_status= +apns_registration= reason= +apns_token_present= +fcm_token_present= +formatted_server_token_prefix=apple_rn-v2 +``` + +## Payload Evidence + +Payload file or sanitized JSON: + +```json +{ + "type": "message", + "server_url": "https://example.invalid", + "server_id": "server-smoke", + "channel_id": "channel-smoke", + "root_id": "", + "is_crt_enabled": "false", + "ack_id": "ack-smoke", + "post_id": "post-smoke" +} +``` + +Send path: + +- Firebase direct FCM: +- Apple Push Notification Console: +- nexo push-proxy/server: +- Local simulator fixture: + +Response ID or log reference: + +```text + +``` + +## Foreground / Background / Open Routing + +| Scenario | Expected | Actual | Evidence path | +|---|---|---|---| +| Foreground receipt | Dart/native event logged | | | +| Background display | System notification shown | | | +| Channel tap | `onNavigateToChannel` with server/channel | | | +| Thread tap | `onNavigateToThread` with server/root | | | +| Terminated tap | Launch payload forwarded | | | + +## ACK, Dismiss, Inline Reply Boundary + +| Feature | Expected for this run | Actual | Follow-up | +|---|---|---|---| +| ACK | implemented | not tested | pending gap | +| Dismiss | implemented | not tested | pending gap | +| Inline reply | implemented | not tested | pending gap | + +## Sanitization Checklist + +- [ ] Full APNs device token removed. +- [ ] Full FCM registration token removed. +- [ ] Apple Team ID, Key ID, certificates, provisioning profile details removed. +- [ ] Firebase API key, app ID, sender ID, service account details removed. +- [ ] Private server URL, auth token, signing key, username, channel names removed. +- [ ] Screenshots or recordings contain no private message/user data. diff --git a/docs/ios-notification-test-guide.md b/docs/ios-notification-test-guide.md index 76854b55..af91b721 100644 --- a/docs/ios-notification-test-guide.md +++ b/docs/ios-notification-test-guide.md @@ -189,15 +189,28 @@ Minimum native bridge work: payloads into Dart. - Map APNs/FCM userInfo fields to `NotificationOpenedEvent`. -Current Dart token formatting uses the Android-oriented prefix: +## iOS Token Contract -```dart -const String _kDeviceTokenPrefix = 'android_rn'; +The current Dart implementation stores Android tokens as +`android_rn-v2:`. For iOS preflight, reserve the +Mattermost-compatible APNs server-registration format: + +```text +apple_rn-v2: ``` -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. +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 @@ -324,6 +337,16 @@ Example local `.apns` fixture shape: } ``` +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. @@ -454,6 +477,9 @@ Do keep: - 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. diff --git a/packages/messaging_flutter/README.md b/packages/messaging_flutter/README.md index e88070c4..126e3efd 100644 --- a/packages/messaging_flutter/README.md +++ b/packages/messaging_flutter/README.md @@ -191,7 +191,8 @@ Handle device-token registration: ```dart NexoMessagingPlugin.instance.onDeviceTokenReady = (String deviceToken) { - // Example format: android_rn-v2:YOUR_FCM_TOKEN + // Android implemented format: android_rn-v2:YOUR_FCM_TOKEN + // iOS preflight server format: apple_rn-v2:YOUR_APNS_TOKEN // Send this token to the nexo server. }; ``` @@ -226,10 +227,14 @@ native storage and native push operations that need those values. `NexoMessagingPlugin.instance.initialize()` during startup. 2. `initialize()` subscribes to native events, listens for FCM token refreshes, requests notification permission, and reads the current FCM token. -3. Dart formats each raw FCM token as `android_rn-v2:` before calling - the native `saveDeviceToken` action. `onDeviceTokenReady` receives the - formatted token, and `getDeviceToken()` returns the last formatted token - stored by native code. +3. Android currently formats each raw FCM token as + `android_rn-v2:` before calling the native `saveDeviceToken` + action. `onDeviceTokenReady` receives the formatted token, and + `getDeviceToken()` returns the last formatted token stored by native code. + iOS preflight reserves `apple_rn-v2:` for Mattermost-compatible + APNs push-proxy registration. An iOS FCM registration token is useful for + Firebase direct-send smoke, but it is not the server registration token unless + a future server/push-proxy path explicitly supports FCM-backed iOS delivery. 4. The host app sends the formatted device token to the nexo server. Native signature verification uses the stored device token and compares the raw FCM token portion with signed push claims. @@ -291,7 +296,7 @@ only depend on the stable fields listed here or on typed SDK objects such as | `message` | `type` | `server_url`, `server_id`, `channel_id`, `root_id`, `is_crt_enabled`, `ack_id`, `post_id`, `signature`, `id_loaded`, `data`, `userInteraction` | `userInteraction: true` routes through opened handling in Dart. If `server_url` is missing, Android may resolve it from `server_id` or the only stored server URL. | | `clear` | `type` | `server_url`, `server_id`, `channel_id`, `root_id` | Used for native notification clear or dismiss handling; extra native fields are forwarded as compatibility data only. | | `session` | `type` | `server_url`, `server_id` | Used for session-related native events; host apps should treat raw fields as compatibility data unless promoted to typed API. | -| `token_refresh` | `type`, `token` | none | `token` is the raw FCM token from native or FlutterFire. Dart formats it as `android_rn-v2:` before native storage and `onDeviceTokenReady`. | +| `token_refresh` | `type`, `token` | `platform_token_type` | `token` is the raw platform token from native or FlutterFire. Android formats it as `android_rn-v2:`. iOS preflight reserves `apple_rn-v2:` for server registration and treats FCM registration tokens as Firebase direct-send smoke tokens unless a future server path supports them. | `NotificationOpenedEvent` parses `server_url`, `channel_id`, `root_id`, and `is_crt_enabled` from opened-compatible events. The parser treats boolean `true`,