feat: iOS notification preflight task and test documentation
- Add iOS notification preflight plan and code review (PLAN/CODE_REVIEW-cloud-G07) - Add iOS notification smoke evidence template - Update iOS notification test guide with preflight steps - Add Flutter test app iOS fixtures (APNS payloads, GoogleService config, entitlements) - Update README files for flutter-test, messaging_flutter, and docs
This commit is contained in:
parent
9f5e8c2343
commit
908a4ecc18
12 changed files with 775 additions and 16 deletions
|
|
@ -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:<raw-apns-token>` 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 알림 테스트
|
||||
- 확인 필요: 없음
|
||||
|
|
|
|||
171
agent-task/m-ios-notification-preflight/CODE_REVIEW-cloud-G07.md
Normal file
171
agent-task/m-ios-notification-preflight/CODE_REVIEW-cloud-G07.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<!-- task=m-ios-notification-preflight plan=0 tag=IOS_BRIDGE -->
|
||||
|
||||
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 |
|
||||
363
agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md
Normal file
363
agent-task/m-ios-notification-preflight/PLAN-cloud-G07.md
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
<!-- task=m-ios-notification-preflight plan=0 tag=IOS_BRIDGE -->
|
||||
|
||||
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
19
apps/flutter-test/ios/Fixtures/open_channel.apns
Normal file
19
apps/flutter-test/ios/Fixtures/open_channel.apns
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
apps/flutter-test/ios/Fixtures/open_thread.apns
Normal file
19
apps/flutter-test/ios/Fixtures/open_thread.apns
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CLIENT_ID</key>
|
||||
<string>REPLACE_WITH_FIREBASE_IOS_CLIENT_ID</string>
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>REPLACE_WITH_REVERSED_CLIENT_ID</string>
|
||||
<key>API_KEY</key>
|
||||
<string>REPLACE_WITH_FIREBASE_API_KEY</string>
|
||||
<key>GCM_SENDER_ID</key>
|
||||
<string>REPLACE_WITH_SENDER_ID</string>
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>REPLACE_WITH_IOS_BUNDLE_ID</string>
|
||||
<key>PROJECT_ID</key>
|
||||
<string>REPLACE_WITH_FIREBASE_PROJECT_ID</string>
|
||||
<key>STORAGE_BUCKET</key>
|
||||
<string>REPLACE_WITH_FIREBASE_STORAGE_BUCKET</string>
|
||||
<key>IS_ADS_ENABLED</key>
|
||||
<false/>
|
||||
<key>IS_ANALYTICS_ENABLED</key>
|
||||
<false/>
|
||||
<key>IS_APPINVITE_ENABLED</key>
|
||||
<false/>
|
||||
<key>IS_GCM_ENABLED</key>
|
||||
<true/>
|
||||
<key>IS_SIGNIN_ENABLED</key>
|
||||
<false/>
|
||||
<key>GOOGLE_APP_ID</key>
|
||||
<string>REPLACE_WITH_FIREBASE_GOOGLE_APP_ID</string>
|
||||
</dict>
|
||||
</plist>
|
||||
8
apps/flutter-test/ios/Runner/Runner.example.entitlements
Normal file
8
apps/flutter-test/ios/Runner/Runner.example.entitlements
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
96
docs/ios-notification-smoke-evidence-template.md
Normal file
96
docs/ios-notification-smoke-evidence-template.md
Normal file
|
|
@ -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: `<redacted-or-placeholder>`
|
||||
- Firebase project/app alias: `<redacted-or-placeholder>`
|
||||
- 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:<redacted-apns-token>`
|
||||
- FCM smoke token used only for Firebase direct send: yes | no | not tested
|
||||
|
||||
## Permission And Token Evidence
|
||||
|
||||
Paste sanitized logs only.
|
||||
|
||||
```text
|
||||
permission_status=<authorized|denied|provisional|not_determined>
|
||||
apns_registration=<success|failure> reason=<redacted-or-placeholder>
|
||||
apns_token_present=<yes|no>
|
||||
fcm_token_present=<yes|no|not_used>
|
||||
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
|
||||
<redacted-or-placeholder>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
@ -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:<raw-fcm-token>`. For iOS preflight, reserve the
|
||||
Mattermost-compatible APNs server-registration format:
|
||||
|
||||
```text
|
||||
apple_rn-v2:<raw-apns-token>
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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:<raw-token>` 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:<raw-fcm-token>` 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:<raw-apns-token>` 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:<raw-token>` 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:<raw-fcm-token>`. iOS preflight reserves `apple_rn-v2:<raw-apns-token>` 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`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue