feat(messaging): Web 알림 API 기준을 추가한다

Flutter Web foreground 알림을 준비하기 위해 초기화 옵션과 token prefix 경계를 public API로 고정한다.

포트/환경 기준 문서와 로드맵 정리, agent-task 리뷰 산출물도 함께 반영한다.
This commit is contained in:
toki 2026-06-07 10:59:12 +09:00
parent 871c792128
commit caafd4f320
17 changed files with 1140 additions and 21 deletions

View file

@ -50,6 +50,21 @@ cp .env.example .env
docker compose up
```
## 워크스페이스 포트 기준
Nexo가 소유한 runtime 기본값은 core HTTP `18065`, push-proxy HTTP
`18066`이다. 두 포트는 외부 소비자가 있을 수 있는 compatibility
baseline이므로 즉시 바꾸지 않는다. Flutter/web preview가 필요해지는
후속 작업에서는 frontend slot `13040`을 우선 후보로 쓰고, backend
포트 이동이 필요하면 `18040` 계열 후보를 별도 migration plan에서
다룬다.
Mattermost upstream 개발 compose가 쓰는 DB/cache/object/search/mail/metrics
포트는 upstream dev auxiliary로 취급한다. Nexo compose는 기본적으로
Postgres를 host에 publish하지 않고 compose network 내부 `db:5432`로만
사용한다. 상세 inventory와 secret boundary는
`services/core/compose/README.md`의 Port And Environment Inventory를 본다.
## 주요 명령
| 목적 | 명령 | 비고 |

View file

@ -11,7 +11,7 @@ Nexo의 upstream-followable Mattermost runtime, push-proxy, Flutter test host, w
## 상태
[진행중]
[완료]
## 승격 조건
@ -35,20 +35,23 @@ Nexo의 upstream-followable Mattermost runtime, push-proxy, Flutter test host, w
Nexo가 소유한 runtime 포트와 upstream 개발 보조 포트를 분리해 정리한다.
- [ ] [runtime-map] core HTTP, push-proxy, Flutter test host, web preview 포트를 host publish와 container/internal 기준으로 나눠 정리한다.
- [ ] [upstream-map] upstream Mattermost dev compose의 DB/cache/object/search/mail/metrics/dashboard 포트를 nexo-owned runtime과 구분해 충돌 후보를 표시한다.
- [ ] [slot-plan] Nexo 기본 후보를 frontend `13040`, backend `18065/18066` 유지, compose-internal DB/cache 우선으로 문서화하고 변경 필요 시 `18040` 계열 후보를 남긴다.
- [ ] [secret-boundary] Firebase, APNs, Mattermost token, private host 값은 tracked 문서에 쓰지 않고 evidence 위치와 책임 경계만 남긴다.
- [x] [runtime-map] core HTTP, push-proxy, Flutter test host, web preview 포트를 host publish와 container/internal 기준으로 나눠 정리한다.
- [x] [upstream-map] upstream Mattermost dev compose의 DB/cache/object/search/mail/metrics/dashboard 포트를 nexo-owned runtime과 구분해 충돌 후보를 표시한다.
- [x] [slot-plan] Nexo 기본 후보를 frontend `13040`, backend `18065/18066` 유지, compose-internal DB/cache 우선으로 문서화하고 변경 필요 시 `18040` 계열 후보를 남긴다.
- [x] [secret-boundary] Firebase, APNs, Mattermost token, private host 값은 tracked 문서에 쓰지 않고 evidence 위치와 책임 경계만 남긴다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 없음
- 상태: 승인됨
- 요청일: 2026-06-07
- 완료 근거:
- `services/core/compose/README.md`에 nexo runtime, upstream dev auxiliary, slot plan, tracked value boundary inventory를 추가했다.
- `README.md`, `apps/flutter-test/README.md`, `docs/runtime-image-validation.md`가 같은 포트/환경 기준을 가리키도록 정리했다.
- 검증: `git diff --check`, `rg --sort path -n '(^## Port And Environment Inventory|^### Nexo Runtime|^### Upstream Dev Services|^### Slot Plan|^### Tracked Value Boundary|워크스페이스 포트 기준|frontend slot `13040`|source of truth|현재 포트)' README.md services/core/compose/README.md apps/flutter-test/README.md docs/runtime-image-validation.md`
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
- [x] 사용자가 완료 결과를 확인했다
- [x] archive 이동을 승인했다
- 리뷰 코멘트: 사용자 요청으로 마일스톤을 종료하고 다음 마일스톤으로 이동한다.
## 범위 제외

View file

@ -34,10 +34,10 @@ Mattermost server/webapp/push-proxy는 upstream-followable runtime으로 두고,
- [완료] Linux 알림 선행 정리
- 경로: `agent-roadmap/archive/phase/messaging-runtime/milestones/linux-notification-preflight.md`
- 요약: Linux 배포판/desktop environment 결정 없이 plugin 등록, notification daemon 후보, bridge 계약, fixture, evidence 기준을 먼저 정리했다.
- [진행중] 워크스페이스 포트/환경 표준화
- 경로: `agent-roadmap/phase/messaging-runtime/milestones/workspace-port-env-standardization.md`
- 요약: Mattermost core, push-proxy, Flutter test/web surface, upstream dev services 포트를 workspace 공통 대역과 remote runtime 문서 기준으로 정리다.
- [계획] Flutter Web 알림
- [완료] 워크스페이스 포트/환경 표준화
- 경로: `agent-roadmap/archive/phase/messaging-runtime/milestones/workspace-port-env-standardization.md`
- 요약: Mattermost core, push-proxy, Flutter test/web surface, upstream dev services 포트를 workspace 공통 대역과 remote runtime 문서 기준으로 정리다.
- [진행중] Flutter Web 알림
- 경로: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- 요약: `packages/messaging_flutter`를 Flutter Web/Chrome foreground browser notification까지 확장하고, Full Web Push는 별도 후속 계약으로 분리한다.
- [계획] iOS 알림 테스트

View file

@ -12,7 +12,7 @@
## 상태
[계획]
[진행중]
## 승격 조건

View file

@ -0,0 +1,194 @@
<!-- task=m-flutter-web-notification/01_api_options plan=0 tag=WEBAPI -->
# Code Review Reference - WEBAPI
> **[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`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
## 개요
date=2026-06-07
task=m-flutter-web-notification/01_api_options, plan=0, tag=WEBAPI
## Roadmap Targets
- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- Task ids:
- `api-options`: 기존 `initialize()` 호출을 깨지 않는 옵션 모델을 추가하고, web notification permission/display 정책과 token prefix 경계를 문서화한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [WEBAPI-1] Initialize Options API | [x] |
| [WEBAPI-2] Web Token Boundary Documentation | [x] |
## 구현 체크리스트
- [x] `NexoMessagingInitializeOptions`와 token prefix boundary 상수를 public Dart API로 추가한다.
- [x] `initialize()`가 기존 no-arg 호출을 유지하면서 옵션으로 FCM permission/token work를 제어하게 한다.
- [x] README에 web foreground와 Full Web Push token prefix 경계를 문서화한다.
- [x] options와 token prefix 단위 테스트를 추가하고 기존 token refresh 테스트를 유지한다.
- [x] `cd packages/messaging_flutter && flutter test`와 `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G04_M.log`로 아카이브한다.
- [x] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-flutter-web-notification/01_api_options/`로 이동한다.
- [x] PASS이면 task group이 `m-flutter-web-notification`임을 런타임 완료 이벤트 메타데이터에 보고한다.
## 계획 대비 변경 사항
계획과 동일한 방식으로 구현했다. 추가적인 변경 없음.
## 주요 설계 결정
1. **`_kDeviceTokenPrefix` 제거**: 기존 `const String _kDeviceTokenPrefix = 'android_rn'`를 제거하고 `_saveDeviceToken()`에서 `NexoMessagingTokenPrefix.androidV2` 직접 사용으로 변경했다. 결과적 토큰 포맷(`android_rn-v2:...`)은 동일하게 유지된다.
2. **`_requestPermission()` 분리**: 기존 `_requestPermission()`가 permission request와 token read를 함께 수행했지만,PLAN에 따라 permission request만 담당하게 하고 token read를 새 private `_saveCurrentDeviceToken()`으로 분리했다.
3. **`initialize()` 시그니처**: 기본 인수를 사용하여 기존 no-arg 호출 (`instance.initialize()`)이 계속 컴파일되도록 보장했다.
4. **`NexoMessagingTokenPrefix` 공개 상수 클래스**: Android, iOS, Web 3개 플랫폼의 토큰 prefix를 상수 클래스로 공개 문서화했다. `webV2`는 미래 Full Web Push용 reserved prefix이다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 아래 `검증 결과` 참조
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `initialize()` no-arg call site가 계속 컴파일되는지 확인한다.
- Android token formatting이 `android_rn-v2:`로 유지되는지 확인한다.
- Web `web_rn-v2:`는 reserved boundary일 뿐 server registration 구현으로 오해되지 않는지 확인한다.
- `Roadmap Targets`가 `api-options` 하나만 완료 대상으로 삼는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### WEBAPI-1 중간 검증
```text
$ cd packages/messaging_flutter && flutter test
00:00 +0: loading /config/workspace/nexo/packages/messaging_flutter/test/nexo_messaging_test.dart
00:00 +0: NotificationOpenedEvent.fromMap parses server_url, channel_id, root_id, is_crt_enabled
00:00 +1: NotificationOpenedEvent.fromMap defaults isCRTEnabled to false when flag is absent or non-true
00:00 +2: NotificationOpenedEvent.fromMap accepts boolean and numeric-string CRT truthy flags
00:00 +3: NexoMessagingPlugin.handleNativeEvent opened event emits NotificationOpenedEvent on stream
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +4: NexoMessagingPlugin.handleNativeEvent message event with userInteraction routes through opened handler
[PushNotification] Opened: channelId=channel-xyz rootId=null
00:00 +5: NexoMessagingPlugin.handleNativeEvent message event without userInteraction does not trigger navigation
00:00 +6: NexoMessagingPlugin.handleNativeEvent opened event with CRT and rootId routes to thread callback
[PushNotification] Opened: channelId=channel-123 rootId=root-789
00:00 +7: NexoMessagingPlugin.handleNativeEvent opened event with boolean CRT and rootId routes to thread callback
[PushNotification] Opened: channelId=channel-123 rootId=root-789
00:00 +8: NexoMessagingPlugin.handleNativeEvent opened event with CRT but empty rootId falls back to channel callback
[PushNotification] Opened: channelId=channel-123 rootId=
00:00 +9: NexoMessagingPlugin.handleNativeEvent opened event without server_url skips navigation callbacks
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +10: NexoMessagingPlugin.handleNativeEvent every event is forwarded to onNotification stream
[PushNotification] Clear: channelId=channel-123
00:00 +11: NexoMessagingPlugin.handleNativeEvent token_refresh event saves formatted device token and invokes callback
[PushNotification] Device token saved
00:00 +12: NexoMessagingPlugin.handleNativeEvent debugSendNativeEventForTesting invokes native debug method
00:00 +13: Channel constants notification and action channel names are preserved
00:00 +14: NexoMessagingInitializeOptions has all default values set to true
00:00 +15: NexoMessagingInitializeOptions can override all fields to false
00:00 +16: NexoMessagingInitializeOptions can override individual fields
00:00 +17: NexoMessagingTokenPrefix androidV2 has the expected value
00:00 +18: NexoMessagingTokenPrefix appleV2 has the expected value
00:00 +19: NexoMessagingTokenPrefix webV2 has the expected value
00:00 +20: All tests passed!
```
### WEBAPI-2 중간 검증
```text
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
Analyzing messaging_flutter...
No issues found! (ran in 2.1s)
```
### 최종 검증
```text
$ cd packages/messaging_flutter && flutter test
00:00 +0: loading /config/workspace/nexo/packages/messaging_flutter/test/nexo_messaging_test.dart
00:00 +0: NotificationOpenedEvent.fromMap parses server_url, channel_id, root_id, is_crt_enabled
00:00 +1: NotificationOpenedEvent.fromMap defaults isCRTEnabled to false when flag is absent or non-true
00:00 +2: NotificationOpenedEvent.fromMap accepts boolean and numeric-string CRT truthy flags
00:00 +3: NexoMessagingPlugin.handleNativeEvent opened event emits NotificationOpenedEvent on stream
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +4: NexoMessagingPlugin.handleNativeEvent message event with userInteraction routes through opened handler
[PushNotification] Opened: channelId=channel-xyz rootId=null
00:00 +5: NexoMessagingPlugin.handleNativeEvent message event without userInteraction does not trigger navigation
00:00 +6: NexoMessagingPlugin.handleNativeEvent opened event with CRT and rootId routes to thread callback
[PushNotification] Opened: channelId=channel-123 rootId=root-789
00:00 +7: NexoMessagingPlugin.handleNativeEvent opened event with boolean CRT and rootId routes to thread callback
[PushNotification] Opened: channelId=channel-123 rootId=root-789
00:00 +8: NexoMessagingPlugin.handleNativeEvent opened event with CRT but empty rootId falls back to channel callback
[PushNotification] Opened: channelId=channel-123 rootId=
00:00 +9: NexoMessagingPlugin.handleNativeEvent opened event without server_url skips navigation callbacks
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +10: NexoMessagingPlugin.handleNativeEvent every event is forwarded to onNotification stream
[PushNotification] Clear: channelId=channel-123
00:00 +11: NexoMessagingPlugin.handleNativeEvent token_refresh event saves formatted device token and invokes callback
[PushNotification] Device token saved
00:00 +12: NexoMessagingPlugin.handleNativeEvent debugSendNativeEventForTesting invokes native debug method
00:00 +13: Channel constants notification and action channel names are preserved
00:00 +14: NexoMessagingInitializeOptions has all default values set to true
00:00 +15: NexoMessagingInitializeOptions can override all fields to false
00:00 +16: NexoMessagingInitializeOptions can override individual fields
00:00 +17: NexoMessagingTokenPrefix androidV2 has the expected value
00:00 +18: NexoMessagingTokenPrefix appleV2 has the expected value
00:00 +19: NexoMessagingTokenPrefix webV2 has the expected value
00:00 +20: All tests passed!
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
Analyzing messaging_flutter...
No issues found! (ran in 2.1s)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS이므로 active plan/review 파일을 로그로 아카이브하고 `complete.log` 작성 후 task directory를 archive로 이동한다.

View file

@ -0,0 +1,42 @@
# Complete - m-flutter-web-notification/01_api_options
## 완료 일시
2026-06-07
## 요약
Flutter Web 알림 선행 API 옵션 작업을 1회 리뷰 루프로 검토했고 최종 판정은 PASS다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | PASS | `NexoMessagingInitializeOptions`, token prefix boundary, README 문서화, 단위 테스트와 analyze 검증이 계획 범위와 일치한다. |
## 구현/정리 내용
- `NexoMessagingInitializeOptions`와 `NexoMessagingTokenPrefix`를 public API로 추가했다.
- `initialize()`가 기존 no-arg 호출을 유지하면서 token refresh, permission request, current token registration을 옵션으로 제어하게 했다.
- Web foreground notification과 future Full Web Push token prefix boundary를 README에 문서화했다.
- options/token prefix 단위 테스트를 추가하고 기존 `android_rn-v2:` token refresh 테스트를 유지했다.
## 최종 검증
- `cd packages/messaging_flutter && flutter test` - PASS; 20개 테스트가 통과했다.
- `cd packages/messaging_flutter && flutter analyze --no-fatal-infos` - PASS; `No issues found!`.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- Completed task ids:
- `api-options`: PASS; evidence=`agent-task/archive/2026/06/m-flutter-web-notification/01_api_options/plan_local_G04_0.log`, `agent-task/archive/2026/06/m-flutter-web-notification/01_api_options/code_review_local_G04_0.log`; verification=`cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,243 @@
<!-- task=m-flutter-web-notification/01_api_options plan=0 tag=WEBAPI -->
# Plan - WEBAPI
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현 후 검증을 실행하고 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 구현 중 사용자만 결정할 수 있는 선택, 사용자 소유 외부 환경/secret, 또는 범위 충돌이 있으면 `사용자 리뷰 요청` 섹션을 근거와 함께 채우고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다.
## 배경
현재 `initialize()`는 인자 없이 Android/FCM permission과 token registration을 항상 수행한다. Flutter Web foreground notification을 추가하려면 기존 호출을 깨지 않으면서 플랫폼별 permission/token 정책을 분리할 옵션 모델이 먼저 필요하다. 이 작업은 첫 Epic의 API 기반 작업이며, 실제 web plugin 등록과 `package:web` interop는 후속 split 작업에서 처리한다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 정당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- Task ids:
- `api-options`: 기존 `initialize()` 호출을 깨지 않는 옵션 모델을 추가하고, web notification permission/display 정책과 token prefix 경계를 문서화한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- `agent-test/local/rules.md`
- `agent-test/local/messaging-flutter-smoke.md`
- `agent-test/local/client-app-smoke.md`
- `packages/messaging_flutter/pubspec.yaml`
- `packages/messaging_flutter/lib/nexo_messaging.dart`
- `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart`
- `packages/messaging_flutter/lib/src/notification_opened_event.dart`
- `packages/messaging_flutter/lib/src/push_notification_type.dart`
- `packages/messaging_flutter/test/nexo_messaging_test.dart`
- `packages/messaging_flutter/README.md`
- `packages/messaging_flutter/analysis_options.yaml`
- `apps/flutter-test/lib/main.dart`
- `apps/flutter-test/test/widget_test.dart`
- `apps/flutter-test/pubspec.yaml`
- `apps/flutter-test/analysis_options.yaml`
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다.
- `packages/messaging_flutter/**` 변경이므로 `agent-test/local/messaging-flutter-smoke.md`를 읽었다.
- 앱 호출부 영향 확인을 위해 `agent-test/local/client-app-smoke.md`도 읽었다.
- 적용 명령: `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`.
- `flutter`와 `dart`는 `/sdk/flutter/bin/` 아래에서 확인했다.
- 외부 Firebase, device, runtime credential은 이 작업 검증에 필요하지 않다.
### 테스트 커버리지 공백
- 새 옵션 value object와 token prefix 상수는 기존 테스트가 없다. 단위 테스트를 추가한다.
- `initialize()`가 no-arg 호출을 유지하는지 컴파일과 analyze로 확인한다.
- 실제 web permission/display 동작은 이 plan의 범위가 아니며 `02+01_web_plugin_baseline`과 후속 foreground Epic에서 다룬다.
### 심볼 참조
- renamed/removed symbol: none.
- 변경될 call site: `apps/flutter-test/lib/main.dart`의 `plugin.initialize()` no-arg 호출은 유지되어야 한다.
### 분할 판단
- split decision policy를 plan 파일 선택 전에 평가했다.
- 첫 Epic은 public API/options와 web platform implementation이 자연스러운 dependency boundary를 가진다.
- shared task group: `m-flutter-web-notification`.
- sibling subtasks:
- `01_api_options`: 독립 선행 작업. 의존 없음.
- `02+01_web_plugin_baseline`: `01_api_options`의 PASS와 `complete.log`가 선행되어야 한다.
- 이 plan은 `api-options`만 완료 대상으로 삼는다. `plugin-web`과 `interop`는 의존 subtask에서 처리한다.
### 범위 결정 근거
- 포함: Dart public options model, initialize 옵션 wiring, token prefix boundary constants/docs, package unit tests.
- 제외: `pubspec.yaml` web platform entry, `package:web` dependency, web implementation file, browser Notification API wrapper, Flutter test web scaffold.
- 제외 이유: web registration/interop는 dependency and platform boundary가 있어 `02+01_web_plugin_baseline`에서 별도 리뷰한다.
### 빌드 등급
- `local-G04`: public API가 추가되지만 범위가 Dart package 내부와 README/test로 제한되고 검증 명령이 결정적이다.
## 구현 체크리스트
- [ ] `NexoMessagingInitializeOptions`와 token prefix boundary 상수를 public Dart API로 추가한다.
- [ ] `initialize()`가 기존 no-arg 호출을 유지하면서 옵션으로 FCM permission/token work를 제어하게 한다.
- [ ] README에 web foreground와 Full Web Push token prefix 경계를 문서화한다.
- [ ] options와 token prefix 단위 테스트를 추가하고 기존 token refresh 테스트를 유지한다.
- [ ] `cd packages/messaging_flutter && flutter test`와 `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## [WEBAPI-1] Initialize Options API
### 문제
[packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart](/config/workspace/nexo/packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:48)의 `initialize()`가 항상 native channel, FCM token refresh, permission request, token read를 한 번에 실행한다. Web foreground notification은 server device token registration과 별도이므로 옵션 모델이 필요하다.
Before:
```dart
48 Future<void> initialize() async {
49 _listenNativeChannel();
50 _listenFcmTokenRefresh();
51 await _requestPermission();
52 }
```
### 해결 방법
새 파일 `packages/messaging_flutter/lib/src/nexo_messaging_options.dart`를 만들고 public export에 추가한다. 기본값은 기존 동작을 보존한다.
After:
```dart
class NexoMessagingInitializeOptions {
const NexoMessagingInitializeOptions({
this.listenForTokenRefresh = true,
this.requestNotificationPermission = true,
this.registerDeviceToken = true,
});
final bool listenForTokenRefresh;
final bool requestNotificationPermission;
final bool registerDeviceToken;
}
class NexoMessagingTokenPrefix {
static const String androidV2 = 'android_rn-v2';
static const String appleV2 = 'apple_rn-v2';
static const String webV2 = 'web_rn-v2';
}
```
`initialize()`는 아래 형태로 바꾼다.
```dart
Future<void> initialize({
NexoMessagingInitializeOptions options =
const NexoMessagingInitializeOptions(),
}) async {
_listenNativeChannel();
if (options.listenForTokenRefresh) {
_listenFcmTokenRefresh();
}
if (options.requestNotificationPermission) {
await _requestPermission();
}
if (options.registerDeviceToken) {
await _saveCurrentDeviceToken();
}
}
```
`_requestPermission()`은 permission request만 담당하게 하고, 기존 token read는 새 private `_saveCurrentDeviceToken()`로 분리한다.
### 수정 파일 및 체크리스트
- [ ] `packages/messaging_flutter/lib/src/nexo_messaging_options.dart` 추가
- [ ] `packages/messaging_flutter/lib/nexo_messaging.dart`에 options export 추가
- [ ] `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart`에 options import와 `initialize()` 옵션 wiring 추가
- [ ] `_saveDeviceToken()`이 `NexoMessagingTokenPrefix.androidV2`를 사용하도록 변경
### 테스트 작성
- `packages/messaging_flutter/test/nexo_messaging_test.dart`에 `NexoMessagingInitializeOptions` 기본값과 override 테스트를 추가한다.
- `NexoMessagingTokenPrefix` 상수 값 테스트를 추가한다.
- 기존 `token_refresh event saves formatted device token` 테스트가 `android_rn-v2:`를 계속 검증하도록 유지한다.
### 중간 검증
```sh
cd packages/messaging_flutter && flutter test
```
기대 결과: 모든 Dart unit test가 통과한다.
## [WEBAPI-2] Web Token Boundary Documentation
### 문제
[packages/messaging_flutter/README.md](/config/workspace/nexo/packages/messaging_flutter/README.md:192)는 Android/iOS token format만 말하고, [packages/messaging_flutter/README.md](/config/workspace/nexo/packages/messaging_flutter/README.md:228)의 token flow도 Web foreground와 Full Web Push의 경계를 설명하지 않는다.
Before:
```md
195 NexoMessagingPlugin.instance.onDeviceTokenReady = (String deviceToken) {
196 // Android implemented format: android_rn-v2:YOUR_FCM_TOKEN
197 // iOS preflight server format: apple_rn-v2:YOUR_APNS_TOKEN
198 // Send this token to the nexo server.
199 };
```
### 해결 방법
README의 Platform Status, Host Integration Contract, Dart Usage, Token And Credential Flow, Public API Contract에 다음 경계를 추가한다.
- Web foreground browser notification은 앱이 열린 상태의 display/open routing API이며 server device-token registration을 만들지 않는다.
- `web_rn-v2:`는 Full Web Push 후속 Milestone에서 service worker/VAPID/core/push-proxy 계약이 생길 때 사용할 reserved prefix다.
- `initialize()`는 no-arg 호출을 유지하되, 옵션으로 permission/token behavior를 제어할 수 있다.
### 수정 파일 및 체크리스트
- [ ] `packages/messaging_flutter/README.md` Platform Status에 Web planned/baseline 설명 추가
- [ ] Dart Usage에 `initialize(options: ...)` 예시 추가
- [ ] Token And Credential Flow에 `web_rn-v2:` reserved boundary 추가
- [ ] Public API Contract에 `NexoMessagingInitializeOptions`와 `NexoMessagingTokenPrefix` 추가
### 테스트 작성
- 문서 변경은 unit test 대상이 아니다. API 테스트는 `WEBAPI-1`에서 작성한다.
### 중간 검증
```sh
cd packages/messaging_flutter && flutter analyze --no-fatal-infos
```
기대 결과: 새 public API와 docs snippets가 analyzer issue 없이 유지된다.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `packages/messaging_flutter/lib/src/nexo_messaging_options.dart` | WEBAPI-1 |
| `packages/messaging_flutter/lib/nexo_messaging.dart` | WEBAPI-1 |
| `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart` | WEBAPI-1 |
| `packages/messaging_flutter/test/nexo_messaging_test.dart` | WEBAPI-1 |
| `packages/messaging_flutter/README.md` | WEBAPI-2 |
## 최종 검증
```sh
cd packages/messaging_flutter && flutter test
cd packages/messaging_flutter && flutter analyze --no-fatal-infos
```
기대 결과: 두 명령 모두 exit code `0`.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,124 @@
<!-- task=m-flutter-web-notification/02+01_web_plugin_baseline plan=0 tag=WEBPLUGIN -->
# Code Review Reference - WEBPLUGIN
> **[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.
> Do not start until predecessor `01_api_options` has `complete.log`.
> 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.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-06-07
task=m-flutter-web-notification/02+01_web_plugin_baseline, plan=0, tag=WEBPLUGIN
## Roadmap Targets
- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- Task ids:
- `plugin-web`: `nexo_messaging`에 Flutter Web plugin 등록과 web 전용 구현 파일을 추가한다.
- `interop`: browser `Notification` API 연동은 `package:web` 기반으로 작성하고, Android/iOS/macOS import와 분리한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [WEBPLUGIN-1] Flutter Web Plugin Registration | [ ] |
| [WEBPLUGIN-2] Package Web Interop Boundary | [ ] |
## 구현 체크리스트
- [ ] 구현 시작 전 `01_api_options` predecessor `complete.log`가 active 또는 archive 경로에 있는지 확인한다.
- [ ] `packages/messaging_flutter/pubspec.yaml``web: ^1.1.1` dependency와 Flutter web platform entry를 추가한다.
- [ ] `NexoMessagingWeb.registerWith()` web 전용 파일을 추가하고 공용 Dart 파일이 `package:web`를 직접 import하지 않게 유지한다.
- [ ] browser `Notification` API 접근은 `package:web` 기반 web-only interop wrapper로 분리한다.
- [ ] web baseline unit/analyze 검증을 추가 또는 보강한다.
- [ ] `cd packages/messaging_flutter && flutter pub get`, `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] predecessor `01_api_options` complete.log가 실제로 있었는지 확인한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G06_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-flutter-web-notification/02+01_web_plugin_baseline/`로 이동한다.
- [ ] PASS이면 task group이 `m-flutter-web-notification`임을 런타임 완료 이벤트 메타데이터에 보고한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `pubspec.yaml` web platform entry가 Flutter docs schema와 맞는지 확인한다.
- `package:web` import가 web-only 파일로 제한되는지 확인한다.
- `plugin-web``interop`만 Roadmap completion 대상으로 기록되는지 확인한다.
- Foreground permission/display/click behavior가 이 plan에서 과하게 구현되지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### WEBPLUGIN-1 중간 검증
```text
$ cd packages/messaging_flutter && flutter pub get
(output)
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
(output)
```
### WEBPLUGIN-2 중간 검증
```text
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
(output)
```
### 최종 검증
```text
$ cd packages/messaging_flutter && flutter pub get
(output)
$ cd packages/messaging_flutter && flutter test
(output)
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
(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?**

View file

@ -0,0 +1,257 @@
<!-- task=m-flutter-web-notification/02+01_web_plugin_baseline plan=0 tag=WEBPLUGIN -->
# Plan - WEBPLUGIN
## 이 파일을 읽는 구현 에이전트에게
이 task는 `agent-task/m-flutter-web-notification/01_api_options/complete.log`가 생긴 뒤 구현한다. `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채워야 완료다. 사용자에게 직접 질문하지 말고, 차단이 있으면 review stub의 `사용자 리뷰 요청`에 근거를 남긴다.
## 배경
Flutter Web plugin 등록과 browser Notification interop는 public initialize/options boundary가 먼저 있어야 한다. 이 plan은 첫 Epic의 `plugin-web``interop`를 묶어 처리하되, foreground permission/display public API는 다음 Epic으로 남긴다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 정당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- Task ids:
- `plugin-web`: `nexo_messaging`에 Flutter Web plugin 등록과 web 전용 구현 파일을 추가한다.
- `interop`: browser `Notification` API 연동은 `package:web` 기반으로 작성하고, Android/iOS/macOS import와 분리한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification.md`
- `agent-test/local/rules.md`
- `agent-test/local/messaging-flutter-smoke.md`
- `agent-test/local/client-app-smoke.md`
- `packages/messaging_flutter/pubspec.yaml`
- `packages/messaging_flutter/lib/nexo_messaging.dart`
- `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart`
- `packages/messaging_flutter/lib/src/notification_opened_event.dart`
- `packages/messaging_flutter/lib/src/push_notification_type.dart`
- `packages/messaging_flutter/test/nexo_messaging_test.dart`
- `packages/messaging_flutter/README.md`
- `packages/messaging_flutter/analysis_options.yaml`
- `apps/flutter-test/lib/main.dart`
- `apps/flutter-test/test/widget_test.dart`
- `apps/flutter-test/pubspec.yaml`
- `apps/flutter-test/analysis_options.yaml`
- Official Flutter docs: `https://docs.flutter.dev/packages-and-plugins/developing-packages`
- Official `package:web` page/API: `https://pub.dev/packages/web`, `https://pub.dev/documentation/web/latest/web/Notification-extension-type.html`
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`, `messaging-flutter-smoke.md`, `client-app-smoke.md`를 읽었다.
- 적용 명령: `cd packages/messaging_flutter && flutter pub get`, `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`.
- Web plugin registration이 consuming app dependency graph에 영향을 주므로 필요 시 `cd apps/flutter-test && flutter pub get`도 실행한다.
- Flutter SDK와 Dart SDK는 `/sdk/flutter/bin/`에서 확인했다.
### 테스트 커버리지 공백
- 기존 tests는 web plugin registration과 browser interop를 검증하지 않는다.
- 이 plan은 web implementation file의 registerWith와 internal interop mapper를 Dart unit/analyze 대상으로 만든다.
- 실제 Chrome permission prompt/display/click smoke는 `foreground``smoke` Epic에서 별도 처리한다.
### 심볼 참조
- renamed/removed symbol: none.
- 새 dependency/call site:
- `packages/messaging_flutter/pubspec.yaml``web: ^1.1.1` 추가.
- `flutter.plugin.platforms.web``pluginClass``fileName` 추가.
- 새 web 전용 파일이 `package:web`를 import한다. Android/iOS/macOS 공용 파일은 `package:web`를 직접 import하지 않는다.
### 분할 판단
- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `m-flutter-web-notification`.
- 이 subtask directory `02+01_web_plugin_baseline`은 predecessor index `01`에 의존한다.
- predecessor status: `agent-task/m-flutter-web-notification/01_api_options/complete.log`는 아직 없다. 구현 시작 전 해당 active sibling 또는 archive candidate complete.log를 확인해야 한다.
- 이 plan은 `plugin-web``interop`를 완료 대상으로 삼고, `permission`, `display`, `event-map`은 후속 Epic으로 남긴다.
### 범위 결정 근거
- 포함: Flutter web platform registration, web implementation entry file, `package:web` dependency, web-only browser Notification interop boundary.
- 제외: permission request public API, browser notification display/click routing behavior, service worker, VAPID, `web_rn-v2` server registration, core/push-proxy platform changes, Flutter test web UI.
- 제외 이유: 첫 Epic baseline은 platform support를 깨지 않게 올리는 기준선이고, user-visible foreground behavior는 다음 Epic에서 검증 단위가 다르다.
### 빌드 등급
- `local-G06`: package manifest, generated plugin registration contract, web-only imports, and dependency resolution risk가 있지만 범위가 package 내부로 제한되고 deterministic Flutter commands로 검증 가능하다.
## 구현 체크리스트
- [ ] 구현 시작 전 `01_api_options` predecessor `complete.log`가 active 또는 archive 경로에 있는지 확인한다.
- [ ] `packages/messaging_flutter/pubspec.yaml``web: ^1.1.1` dependency와 Flutter web platform entry를 추가한다.
- [ ] `NexoMessagingWeb.registerWith()` web 전용 파일을 추가하고 공용 Dart 파일이 `package:web`를 직접 import하지 않게 유지한다.
- [ ] browser `Notification` API 접근은 `package:web` 기반 web-only interop wrapper로 분리한다.
- [ ] web baseline unit/analyze 검증을 추가 또는 보강한다.
- [ ] `cd packages/messaging_flutter && flutter pub get`, `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
1. `agent-task/m-flutter-web-notification/01_api_options/complete.log` 또는 archive의 matching `01_api_options/complete.log`를 확인한다.
2. predecessor가 없으면 구현하지 말고 review stub의 `사용자 리뷰 요청`이 아니라 계획 미충족으로 보고한다.
3. predecessor가 있으면 이 plan의 구현을 진행한다.
## [WEBPLUGIN-1] Flutter Web Plugin Registration
### 문제
[packages/messaging_flutter/pubspec.yaml](/config/workspace/nexo/packages/messaging_flutter/pubspec.yaml:21)은 Android/iOS/macOS만 등록되어 있어 Flutter Web app이 plugin registrant를 생성할 수 없다. Flutter 공식 문서는 web plugin Dart implementation에 `pluginClass``fileName`을 사용한다고 설명한다.
Before:
```yaml
21 flutter:
22 plugin:
23 platforms:
24 android:
25 package: com.tokilabs.nexo.messaging
26 pluginClass: NexoMessagingPlugin
27 ios:
28 pluginClass: NexoMessagingPlugin
29 macos:
30 pluginClass: NexoMessagingPlugin
```
### 해결 방법
`pubspec.yaml`에 web dependency와 platform entry를 추가한다.
```yaml
dependencies:
flutter:
sdk: flutter
firebase_messaging: ^15.2.5
web: ^1.1.1
flutter:
plugin:
platforms:
web:
pluginClass: NexoMessagingWeb
fileName: src/nexo_messaging_web.dart
```
`lib/src/nexo_messaging_web.dart`를 추가한다.
```dart
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
class NexoMessagingWeb {
static void registerWith(Registrar registrar) {
// Baseline registration; foreground behavior is wired in later tasks.
}
}
```
### 수정 파일 및 체크리스트
- [ ] `packages/messaging_flutter/pubspec.yaml` dependency/platform entry 추가
- [ ] `packages/messaging_flutter/lib/src/nexo_messaging_web.dart` 추가
- [ ] `packages/messaging_flutter/pubspec.lock` 갱신 여부 확인. package plugin lockfile은 이 repo에서 ignored이므로 source diff에 남기지 않는다.
### 테스트 작성
- unit test가 web registrant generation 자체를 실행하지는 않는다.
- `flutter pub get``flutter analyze --no-fatal-infos`로 pubspec schema/import correctness를 검증한다.
### 중간 검증
```sh
cd packages/messaging_flutter && flutter pub get
cd packages/messaging_flutter && flutter analyze --no-fatal-infos
```
기대 결과: dependency resolution과 analyzer가 통과한다.
## [WEBPLUGIN-2] Package Web Interop Boundary
### 문제
[packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart](/config/workspace/nexo/packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:3)는 공용 Dart API에서 `firebase_messaging`를 직접 import하고 있고, browser `Notification` API 경계가 없다. `package:web`는 Dart web interop의 장기 해법이며, `Notification.permission``Notification.requestPermission()` API를 제공한다.
### 해결 방법
`package:web` import는 web-only implementation 아래로 제한한다. 예시 구조:
```text
lib/src/web/browser_notification_interop.dart
lib/src/web/browser_notification_interop_web.dart
```
`browser_notification_interop_web.dart`만 아래 import를 가진다.
```dart
import 'dart:js_interop';
import 'package:web/web.dart' as web;
```
첫 Epic에서는 public permission/display API를 만들지 않는다. 대신 future foreground task가 사용할 private wrapper만 둔다.
```dart
class BrowserNotificationInterop {
const BrowserNotificationInterop();
String get permission => web.Notification.permission.toString();
Future<String> requestPermission() async {
final result = await web.Notification.requestPermission().toDart;
return result.toDart;
}
}
```
실제 display/click wiring은 `permission`, `display`, `event-map` Task에서 구현한다.
### 수정 파일 및 체크리스트
- [ ] `packages/messaging_flutter/lib/src/web/browser_notification_interop.dart` 추가
- [ ] `packages/messaging_flutter/lib/src/web/browser_notification_interop_web.dart` 추가
- [ ] non-web common API가 `package:web`를 직접 import하지 않는지 확인
- [ ] README Platform Status 또는 Current Limitations에 Web baseline registration 상태를 반영
### 테스트 작성
- wrapper가 web-only인 경우 VM unit test에서 직접 import하지 않는다.
- analyzer로 import boundary를 검증한다.
- 가능하면 web implementation file의 non-browser pure mapper만 별도 testable helper로 분리한다. 실제 Notification prompt/display는 이 plan의 테스트 대상이 아니다.
### 중간 검증
```sh
cd packages/messaging_flutter && flutter analyze --no-fatal-infos
```
기대 결과: non-web analyzer에서도 `package:web` import가 공용 VM test path를 깨지 않는다.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `packages/messaging_flutter/pubspec.yaml` | WEBPLUGIN-1 |
| `packages/messaging_flutter/lib/src/nexo_messaging_web.dart` | WEBPLUGIN-1 |
| `packages/messaging_flutter/lib/src/web/browser_notification_interop.dart` | WEBPLUGIN-2 |
| `packages/messaging_flutter/lib/src/web/browser_notification_interop_web.dart` | WEBPLUGIN-2 |
| `packages/messaging_flutter/README.md` | WEBPLUGIN-2 |
## 최종 검증
```sh
cd packages/messaging_flutter && flutter pub get
cd packages/messaging_flutter && flutter test
cd packages/messaging_flutter && flutter analyze --no-fatal-infos
```
기대 결과: 세 명령 모두 exit code `0`.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -68,6 +68,11 @@ Run on a connected device or emulator:
flutter run
```
This harness does not own a default host HTTP port. Android/iOS/device smoke
tests should receive runtime URLs from the invoking shell or ignored local
profiles. If a browser preview is added later, use the workspace frontend slot
`13040` unless a later migration plan changes the baseline.
For Android FCM smoke testing, make sure the Android app has the required
Firebase configuration, including `android/app/google-services.json`.
Use the plugin-owned

View file

@ -1,6 +1,10 @@
# Nexo Runtime Image 검증
이 문서는 nexo-owned runtime image의 registry, tag, architecture, verification digest를 기록한다.
현재 포트와 환경 기준의 원천은
`../services/core/compose/README.md`의 Port And Environment Inventory다. 이
문서의 명령 출력은 당시 검증 증거이며, 새 private host, token,
credential, concrete endpoint 값을 추가 기록하지 않는다.
## 게시된 Image 요약

View file

@ -53,6 +53,7 @@ copying upstream mobile code.
| iOS | Stub | Plugin scaffold exists, behavior is not implemented yet |
| macOS | Stub | Plugin scaffold exists; user-provided signing, machine, APNs/Firebase scope, and evidence handoff requirements are documented in [`../../docs/macos-notification-test-guide.md`](../../docs/macos-notification-test-guide.md) |
| Windows | Not registered | No Flutter plugin scaffold yet; user-provided Windows runner, packaging, identity, signing, and evidence handoff requirements are documented in [`../../docs/windows-notification-test-guide.md`](../../docs/windows-notification-test-guide.md) |
| Web | Planned | Browser foreground notification (display/open routing while tab is active) is the first web milestone. Full Web Push (service worker, VAPID, server device-token registration) follows. API baseline (`NexoMessagingInitializeOptions`, token prefix boundaries) is available in this version. |
## Repository Layout
@ -153,6 +154,23 @@ Initialize the plugin:
await NexoMessagingPlugin.instance.initialize();
```
The plugin initializes with default options that match the previous behaviour
(subscribes to native events, listens for FCM token refreshes, requests
notification permission, and registers the device token).
To control which initialisation steps run, pass
`NexoMessagingInitializeOptions`:
```dart
await NexoMessagingPlugin.instance.initialize(
options: const NexoMessagingInitializeOptions(
listenForTokenRefresh: true,
requestNotificationPermission: true,
registerDeviceToken: true,
),
);
```
Listen to raw notification events:
```dart
@ -237,6 +255,8 @@ native storage and native push operations that need those values.
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.
Web (Full Push) reserves `web_rn-v2:<raw-push-token>` for service-worker
registration; see the boundary note below.
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.
@ -258,6 +278,27 @@ when exactly one server is known. Host apps should still register device tokens
with the server after `onDeviceTokenReady` and should not parse secret values or
raw native storage details from README examples.
## Web Token Boundary
Web support is delivered in two milestones. Understanding the boundary between
them is important for server registration and token-format expectations.
**Browser foreground notification** (the first web milestone) is a display/open
routing API for when the tab is active. It does **not** create a server device-
token registration because the browser already knows how to reach the client
through the open connection.
**Full Web Push** (a future milestone) will introduce service-worker handling,
VAPID keys, and a `core/push-proxy` contract. That milestone will use the
reserved token prefix `web_rn-v2:<raw-push-token>` for server device-token
registration. Until that milestone lands:
- `NexoMessagingTokenPrefix.webV2` is a documentation-only constant.
- No web implementation files, `package:web` dependency, or browser
`Notification.requestPermission()` / `Notification.showNotification()` calls
are present in this package.
- Those are tracked under the subsequent `02+01_web_plugin_baseline` task.
## Public API Contract
Apps should import only `package:nexo_messaging/nexo_messaging.dart`. The
@ -278,6 +319,8 @@ they are listed here.
| `NotificationOpenedEvent` | Keep | Stable Dart value object for opened notification routing fields. |
| `PushNotificationType` | Compatibility | Native event string constants. Future typed event wrappers should reduce direct app branching on these constants. |
| `notificationChannelName`, `actionChannelName`, `listenNativeChannelForTesting()`, `debugSendNativeEventForTesting()`, `handleNativeEvent()` | Testing-only | Exposed for repository-owned tests through `@visibleForTesting`; consuming apps should not depend on them. |
| `NexoMessagingInitializeOptions` | New | Options class for `initialize()`. Default values preserve backward-compatible behaviour. |
| `NexoMessagingTokenPrefix` | New | Static constants (`androidV2`, `appleV2`, `webV2`) documenting the token-prefix format for each platform target. |
Deprecated candidates are compatibility-only app dependencies on raw
`onNotification` maps, direct branching on `PushNotificationType`, and parsing
@ -609,5 +652,8 @@ Evidence to capture:
- Android is the only implemented runtime target.
- iOS and macOS are scaffolded but no-op.
- Web is planned: API baseline (`NexoMessagingInitializeOptions`, token prefix
constants) is ready; browser foreground notification and Full Web Push
implementation are tracked in subsequent milestones.
- Full FCM delivery cannot be reliably automated in a headless test runner.
- The `apps/flutter-test/` app should stay focused on package integration behavior.

View file

@ -1,3 +1,4 @@
export 'src/nexo_messaging_options.dart';
export 'src/nexo_messaging_plugin.dart';
export 'src/notification_opened_event.dart';
export 'src/push_notification_type.dart';

View file

@ -0,0 +1,49 @@
/// Options controlling which platform-specific initialization tasks
/// [NexoMessagingPlugin.initialize()] performs.
///
/// Default values preserve the previous behaviour where [initialize()] always
/// listens for FCM token refreshes, requests notification permission, and
/// registers the device token.
class NexoMessagingInitializeOptions {
const NexoMessagingInitializeOptions({
this.listenForTokenRefresh = true,
this.requestNotificationPermission = true,
this.registerDeviceToken = true,
});
/// Whether to subscribe to FCM token refresh events.
///
/// When `false` the plugin will not call [_listenFcmTokenRefresh].
final bool listenForTokenRefresh;
/// Whether to request notification permissions from the platform.
///
/// When `false` the plugin will not call [_requestPermission].
final bool requestNotificationPermission;
/// Whether to read the current device token and emit it through
/// [NexoMessagingPlugin.onDeviceTokenReady] / native storage.
///
/// When `false` the plugin will not call [_saveCurrentDeviceToken].
final bool registerDeviceToken;
}
/// Token-prefix boundaries for different platform targets.
///
/// These constants document the format the SDK uses (or will use) when
/// forwarding device tokens to the host app via [NexoMessagingPlugin.onDeviceTokenReady]
/// or native storage.
///
/// - `androidV2` is the currently implemented prefix for Android FCM tokens.
/// - `appleV2` is reserved for future iOS APNs/FCM server registration.
/// - `webV2` is reserved for Full Web Push (service-worker / VAPID) in a
/// future milestone. **Web foreground notification** (browser display/Open
/// routing while the tab is active) does NOT create a server device-token
/// registration and therefore does not use this prefix.
///
/// Example: `android_rn-v2:YOUR_FCM_TOKEN`
class NexoMessagingTokenPrefix {
static const String androidV2 = 'android_rn-v2';
static const String appleV2 = 'apple_rn-v2';
static const String webV2 = 'web_rn-v2';
}

View file

@ -4,6 +4,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'nexo_messaging_options.dart';
import 'notification_opened_event.dart';
import 'push_notification_type.dart';
@ -11,7 +12,6 @@ const String _kNotificationChannelName =
'com.tokilabs.nexo.messaging/notifications';
const String _kActionChannelName =
'com.tokilabs.nexo.messaging/notification_actions';
const String _kDeviceTokenPrefix = 'android_rn';
class NexoMessagingPlugin {
NexoMessagingPlugin._();
@ -45,10 +45,27 @@ class NexoMessagingPlugin {
void Function(String serverUrl, String rootId)? onNavigateToThread;
void Function(String deviceToken)? onDeviceTokenReady;
Future<void> initialize() async {
/// Initialize the messaging plugin.
///
/// By default ([options] unchanged) the plugin subscribes to native events,
/// listens for FCM token refreshes, requests notification permission, and
/// registers the device token matching the pre-Version-0.0.2 behaviour.
///
/// Pass [NexoMessagingInitializeOptions] to disable any of those steps.
Future<void> initialize({
NexoMessagingInitializeOptions options =
const NexoMessagingInitializeOptions(),
}) async {
_listenNativeChannel();
_listenFcmTokenRefresh();
await _requestPermission();
if (options.listenForTokenRefresh) {
_listenFcmTokenRefresh();
}
if (options.requestNotificationPermission) {
await _requestPermission();
}
if (options.registerDeviceToken) {
await _saveCurrentDeviceToken();
}
}
void dispose() {
@ -95,7 +112,11 @@ class NexoMessagingPlugin {
debugPrint(
'[PushNotification] Permission: ${settings.authorizationStatus}',
);
}
/// Reads the current FCM/APNs token and saves it via [_saveDeviceToken].
/// Called during [initialize] when [NexoMessagingInitializeOptions.registerDeviceToken] is `true`.
Future<void> _saveCurrentDeviceToken() async {
final token = await FirebaseMessaging.instance.getToken();
if (token != null) await _saveDeviceToken(token);
}
@ -208,7 +229,7 @@ class NexoMessagingPlugin {
Future<void> _saveDeviceToken(String token) async {
try {
final formattedToken = '$_kDeviceTokenPrefix-v2:$token';
final formattedToken = '${NexoMessagingTokenPrefix.androidV2}:$token';
await _actionChannel.invokeMethod('saveDeviceToken', {
'token': formattedToken,
});

View file

@ -287,4 +287,55 @@ void main() {
);
});
});
group('NexoMessagingInitializeOptions', () {
test('has all default values set to true', () {
const options = NexoMessagingInitializeOptions();
expect(options.listenForTokenRefresh, isTrue);
expect(options.requestNotificationPermission, isTrue);
expect(options.registerDeviceToken, isTrue);
});
test('can override all fields to false', () {
const options = NexoMessagingInitializeOptions(
listenForTokenRefresh: false,
requestNotificationPermission: false,
registerDeviceToken: false,
);
expect(options.listenForTokenRefresh, isFalse);
expect(options.requestNotificationPermission, isFalse);
expect(options.registerDeviceToken, isFalse);
});
test('can override individual fields', () {
const options1 = NexoMessagingInitializeOptions(
requestNotificationPermission: false,
);
expect(options1.listenForTokenRefresh, isTrue);
expect(options1.requestNotificationPermission, isFalse);
expect(options1.registerDeviceToken, isTrue);
const options2 = NexoMessagingInitializeOptions(
listenForTokenRefresh: false,
registerDeviceToken: false,
);
expect(options2.listenForTokenRefresh, isFalse);
expect(options2.requestNotificationPermission, isTrue);
expect(options2.registerDeviceToken, isFalse);
});
});
group('NexoMessagingTokenPrefix', () {
test('androidV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.androidV2, 'android_rn-v2');
});
test('appleV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.appleV2, 'apple_rn-v2');
});
test('webV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.webV2, 'web_rn-v2');
});
});
}

View file

@ -26,6 +26,70 @@ test deployment can run beside the existing Mattermost service.
The Docker network subnet is fixed to `172.38.0.0/16`. Treat this range as
reserved for nexo and do not reuse it for other Docker networks.
## Port And Environment Inventory
This compose file is the source of truth for the nexo-owned runtime port shape.
It keeps public development defaults stable, while private hostnames,
credentials, tokens, and concrete remote endpoints stay in ignored environment
profiles or private run logs.
### Nexo Runtime
| Surface | Host publish | Container or internal target | Notes |
| --- | --- | --- | --- |
| Core HTTP and webapp | `${NEXO_CORE_PORT:-18065}` | `core:8065` | Compatibility baseline. Keep as-is unless a migration plan updates consumers. |
| Push-proxy HTTP | `${NEXO_PUSH_PROXY_PORT:-18066}` | `push-proxy:8066` | Host publish is for smoke and diagnostics. Core talks to `http://push-proxy:8066` on the compose network. |
| Postgres | none | `db:5432` | Compose-internal only. Do not publish by default. |
| Flutter test host | none | device/emulator app process | Runtime URL values belong in ignored local profiles or the invoking shell. |
| Flutter web preview | `13040` candidate | future web host | Reserved frontend slot for a future web preview; this repository currently does not publish a Flutter web host by default. |
### Upstream Dev Services
The Mattermost upstream development compose files under
`services/core/server/` publish several common local ports. Treat them as
upstream dev auxiliaries, not nexo runtime ports.
| Upstream service | Typical host publish | Role |
| --- | --- | --- |
| Mattermost dev proxy/server | `8065` | Upstream server/webapp development entrypoint |
| PostgreSQL | `5432` | Upstream dev database |
| Redis | `6379` | Upstream cache/session auxiliary |
| MinIO / console | `9000`, `9002` | Upstream object storage and console |
| Azurite | `10000` | Upstream Azure storage emulator |
| Inbucket web / SMTP / POP3 | `9001`, `10025`, `10110` | Upstream mail testing |
| OpenLDAP | `389`, `636` | Upstream LDAP testing |
| Elasticsearch / transport | `9200`, `9300` | Upstream search testing |
| OpenSearch | `9201` | Upstream search testing |
| DejaVu | `1358` | Upstream search dashboard |
| Keycloak | `8484 -> 8080` | Upstream SAML/OIDC testing |
| Prometheus | `9090` | Upstream metrics testing |
| Grafana | `3000` | Upstream dashboard testing |
| Loki | `3100` | Upstream log aggregation testing |
| OTel collector | `13133` | Upstream telemetry collector health |
If these upstream ports conflict with local services, change the upstream dev
compose invocation or environment for that run. Do not move nexo runtime
defaults just to satisfy an upstream dev auxiliary conflict.
### Slot Plan
- Frontend preview slot: reserve `13040` for a future Flutter/web preview.
- Backend compatibility slots: keep core `18065` and push-proxy `18066`.
- Compose-internal auxiliaries: keep DB/cache/object storage private to the
compose network unless a separate plan requires host access.
- Future backend migration candidate: if `18065`/`18066` must move, use a
planned `18040` range migration with compatibility notes and consumer smoke
evidence instead of changing defaults in place.
### Tracked Value Boundary
Tracked files may name environment variable names, example paths, sanitized
profile labels, and non-secret public development defaults. Do not commit raw
Firebase or APNs files, Mattermost tokens, passwords, private hostnames,
private ports, or concrete remote endpoint URLs. Store those values in ignored
`.env` files, ignored runtime data, or private test profiles, and refer to
sanitized evidence locations from public reports.
## Secret Boundary
Commit only `.env.example`. Keep `.env`, runtime data, logs, push-proxy