fix(messaging): Web 알림 클릭 라우팅을 연결한다
실제 browser Notification 인스턴스에 click listener가 없어 foreground 알림 클릭이 opened routing으로 이어지지 않았다. 리뷰 루프를 PASS로 종결하고 task archive evidence를 남긴다.
This commit is contained in:
parent
098abcaa99
commit
3620da91b7
7 changed files with 194 additions and 60 deletions
|
|
@ -42,47 +42,51 @@ task=m-flutter-web-notification/04+02,03_display, plan=1, tag=REVIEW_WEBDISPLAY
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_WEBDISPLAY-1] Real Browser Click Bridge | [ ] |
|
||||
| [REVIEW_WEBDISPLAY-1] Real Browser Click Bridge | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `BrowserNotificationInteropImpl.show()`가 생성한 실제 `web.Notification` 객체에 click handler를 연결하고, 표시 시점의 routing data를 click callback으로 전달한다.
|
||||
- [ ] `BrowserNotificationInterop` boundary를 dumb interface로 유지하고, `package:web` import가 web-only 파일 밖으로 새지 않게 한다.
|
||||
- [ ] fake 기반 tests 또는 기존 tests를 보강해 `showWebForegroundNotification()`의 display/click routing contract가 유지되는지 검증한다.
|
||||
- [ ] README의 Web foreground 상태와 Current Limitations 문구가 서로 모순되지 않게 정리한다.
|
||||
- [ ] `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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
- [x] `BrowserNotificationInteropImpl.show()`가 생성한 실제 `web.Notification` 객체에 click handler를 연결하고, 표시 시점의 routing data를 click callback으로 전달한다.
|
||||
- [x] `BrowserNotificationInterop` boundary를 dumb interface로 유지하고, `package:web` import가 web-only 파일 밖으로 새지 않게 한다.
|
||||
- [x] fake 기반 tests 또는 기존 tests를 보강해 `showWebForegroundNotification()`의 display/click routing contract가 유지되는지 검증한다.
|
||||
- [x] README의 Web foreground 상태와 Current Limitations 문구가 서로 모순되지 않게 정리한다.
|
||||
- [x] `cd packages/messaging_flutter && flutter pub get`, `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G09_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_local_G09_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-flutter-web-notification/04+02,03_display/`를 `agent-task/archive/YYYY/MM/m-flutter-web-notification/04+02,03_display/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-flutter-web-notification`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-flutter-web-notification/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G09_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_local_G09_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/m-flutter-web-notification/04+02,03_display/`를 `agent-task/archive/YYYY/MM/m-flutter-web-notification/04+02,03_display/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-flutter-web-notification`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-flutter-web-notification/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.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로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
- `_buildNotificationOptions`에서 `web.NotificationOptions` builder 대신 raw JS object construction을 사용하지 않고, 표준 `web.NotificationOptions(body, icon, tag)`를 그대로 사용했다. routing data는 `NotificationOptions.data` 필드 대신 Dart closure `_lastRoutingData`로 capture하는 방식으로 구현했다. 이는 `package:web`의 `NotificationOptions` 생성자가 `data` 필드를 노출하지 않아 JS interop 없이 전달할 수 없기 때문이다.
|
||||
- `isSupported` 체크에서 `globalContext.has('Notification')` 대신 `web.window.hasProperty('Notification'.toJS).toDart`를 사용했다. `globalContext`는 `dart:js_interop`에서 직접 접근할 수 없고 `dart:js_interop_unsafe`가 필요하기 때문에 `web.window`로 우회했다.
|
||||
- `_attachClickHandler`에서 `addEventListener`는 `web.EventOptions` 대신 `bool` 세 번째 인자를 사용한다. `dart:js_interop`의 `addEventListener` 시그니처가 `(String, JSFunction, bool)`이다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
- **routing data 전달 방식**: `show()` 호출 시점의 `options` map에서 routing fields(`server_url`, `channel_id`, `root_id`, `is_crt_enabled`)를 추출해 `_lastRoutingData`에 저장한다. `_attachClickHandler`에서 `notification.addEventListener('click', ...)`로 listener를 붙일 때 이 `_lastRoutingData`를 Dart closure로 capture한다. 이 방식은 `package:web`의 `NotificationOptions`에 `data` 필드가 없어 JS interop로 설정하는 복잡한 우회 과정이 필요하지 않으며, click 이벤트 시 가장 최근의 display에 사용된 routing data를 정확히 전달한다.
|
||||
|
||||
- **기존 `_registerClickHandler()` 유지**: `NexoMessagingPlugin._registerClickHandler()`의 기존 구조를 그대로 유지한다. `BrowserNotificationInteropImpl._clickHandler`는 단순히 `NexoMessagingPlugin`에서 설정한 callback을 저장하고, click 이벤트 시 `handleNativeEvent(openedData)`를 호출하는 기존 경로를 재사용한다.
|
||||
|
||||
- **`package:web` import 경계**: `import 'dart:js_interop_unsafe'`와 `import 'package:web/web.dart' as web`는 `browser_notification_interop_web.dart` 파일에만 존재한다. `browser_notification_interop.dart` (abstract interface)와 `browser_notification_interop_stub.dart`는 `package:web` import가 없다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
|
|
@ -112,23 +116,69 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
|||
|
||||
```text
|
||||
$ cd packages/messaging_flutter && flutter test
|
||||
(output)
|
||||
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
|
||||
00:00 +4: NexoMessagingPlugin.handleNativeEvent message event with userInteraction routes through opened handler
|
||||
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
|
||||
00:00 +7: NexoMessagingPlugin.handleNativeEvent opened event with boolean CRT and rootId routes to thread callback
|
||||
00:00 +8: NexoMessagingPlugin.handleNativeEvent opened event with CRT but empty rootId falls back to channel callback
|
||||
00:00 +9: NexoMessagingPlugin.handleNativeEvent opened event without server_url skips navigation callbacks
|
||||
00:00 +10: NexoMessagingPlugin.handleNativeEvent every event is forwarded to onNotification stream
|
||||
00:00 +11: NexoMessagingPlugin.handleNativeEvent token_refresh event saves formatted device token and invokes callback
|
||||
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: NexoMessagingPlugin web notification permission reports unsupported status on non-web platforms
|
||||
00:00 +18: NexoMessagingPlugin web notification permission request is a no-op unsupported status on non-web platforms
|
||||
00:00 +19: NexoMessagingTokenPrefix androidV2 has the expected value
|
||||
00:00 +20: NexoMessagingTokenPrefix appleV2 has the expected value
|
||||
00:00 +21: NexoMessagingTokenPrefix webV2 has the expected value
|
||||
00:00 +22: normalizeWebForegroundMessage missing type defaults to PushNotificationType.message
|
||||
00:00 +23: normalizeWebForegroundMessage existing type is preserved
|
||||
00:00 +24: normalizeWebForegroundMessage userInteraction defaults to false when absent from payload
|
||||
00:00 +25: normalizeWebForegroundMessage userInteraction parameter is applied when payload does not set it
|
||||
00:00 +26: normalizeWebForegroundMessage userInteraction in payload overrides parameter
|
||||
00:00 +27: normalizeWebForegroundMessage routing fields are preserved for NotificationOpenedEvent.fromMap
|
||||
00:00 +28: normalizeWebForegroundMessage null routing field values are not included in result
|
||||
00:00 +29: normalizeWebForegroundMessage absent routing fields are not included in result
|
||||
00:00 +30: normalizeWebForegroundMessage message stable fields are preserved in normalized result
|
||||
00:00 +31: normalizeWebForegroundMessage unknown extra fields are preserved
|
||||
00:00 +32: showWebForegroundNotification fake interop unsupported platform returns unsupported result
|
||||
00:00 +33: showWebForegroundNotification fake interop denied permission returns no-op result
|
||||
00:00 +34: showWebForegroundNotification fake interop granted triggers interop.show()
|
||||
00:00 +35: showWebForegroundNotification fake interop interop.show() receives routing fields
|
||||
00:00 +36: showWebForegroundNotification fake interop click routes onNotificationOpened + onNavigateToChannel
|
||||
00:00 +37: showWebForegroundNotification fake interop click routes onNavigateToThread when CRT
|
||||
00:00 +38: showWebForegroundNotification fake interop click emits opened event on stream
|
||||
00:00 +39: showWebForegroundNotification fake interop display forwards to onNotification stream
|
||||
00:00 +40: All tests passed!
|
||||
|
||||
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
|
||||
(output)
|
||||
Analyzing messaging_flutter...
|
||||
No issues found!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ cd packages/messaging_flutter && flutter pub get
|
||||
(output)
|
||||
Resolving dependencies...
|
||||
Got dependencies!
|
||||
|
||||
$ cd packages/messaging_flutter && flutter test
|
||||
(output)
|
||||
00:00 +0: loading /config/workspace/nexo/packages/messaging_flutter/test/nexo_messaging_test.dart
|
||||
...
|
||||
00:00 +40: All tests passed!
|
||||
|
||||
$ cd packages/messaging_flutter && flutter analyze --no-fatal-infos
|
||||
(output)
|
||||
Analyzing messaging_flutter...
|
||||
No issues found!
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -151,3 +201,17 @@ Sections and their ownership:
|
|||
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
|
||||
| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움 |
|
||||
| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS이므로 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Complete - m-flutter-web-notification/04+02,03_display
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-07
|
||||
|
||||
## 요약
|
||||
|
||||
Web foreground browser notification display의 실제 `web.Notification` click bridge를 보완한 두 번째 리뷰 루프이며 최종 판정은 PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G08_0.log` | `code_review_local_G08_0.log` | FAIL | 실제 `BrowserNotificationInteropImpl`가 생성한 browser notification에 click listener를 연결하지 않아 follow-up 필요 |
|
||||
| `plan_local_G09_1.log` | `code_review_local_G09_1.log` | PASS | 실제 `web.Notification` 인스턴스에 click listener를 붙이고 routing data를 기존 opened routing으로 전달 |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `BrowserNotificationInteropImpl.show()`가 생성한 `web.Notification` 인스턴스에 native `click` listener를 연결하고, 표시 시점의 `server_url`, `channel_id`, `root_id`, `is_crt_enabled` routing data를 click callback으로 전달했다.
|
||||
- `package:web` import 경계는 web-only implementation 파일에 유지했고, common interface는 dumb boundary로 남겼다.
|
||||
- README의 Web foreground 상태 설명을 실제 foreground display, stream forwarding, click routing 구현 상태와 맞췄다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd packages/messaging_flutter && flutter pub get` - PASS; dependency resolution completed with "Got dependencies!".
|
||||
- `cd packages/messaging_flutter && flutter test` - PASS; 40 tests passed.
|
||||
- `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:
|
||||
- `display`: PASS; evidence=`plan_local_G09_1.log`, `code_review_local_G09_1.log`; verification=`cd packages/messaging_flutter && flutter pub get`, `cd packages/messaging_flutter && flutter test`, `cd packages/messaging_flutter && flutter analyze --no-fatal-infos`
|
||||
- Not completed task ids: 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -700,7 +700,8 @@ Evidence to capture:
|
|||
- Web baseline (`NexoMessagingWeb.registerWith()`, `BrowserNotificationInterop`,
|
||||
`package:web` dependency) is implemented, and the foreground display API
|
||||
(`showWebForegroundNotification`) with click routing is implemented.
|
||||
- Notification events are not yet forwarded through the main plugin stream.
|
||||
- Notification events are forwarded through the main plugin stream.
|
||||
- Click events are wired to `handleNativeEvent()` → `onNotificationOpened` / `onNavigateToChannel` / `onNavigateToThread`. The underlying `BrowserNotificationInteropImpl` now attaches a native `click` event listener on the `web.Notification` instance, so actual browser notification click events trigger the full routing pipeline (not just fake tests anymore).
|
||||
- Full Web Push (service worker, VAPID, server device-token registration) is
|
||||
not implemented.
|
||||
- Full FCM delivery cannot be reliably automated in a headless test runner.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
BrowserNotificationInteropImpl();
|
||||
|
||||
@override
|
||||
bool get isSupported => globalContext.has('Notification');
|
||||
bool get isSupported {
|
||||
final win = web.window;
|
||||
return win.hasProperty('Notification'.toJS).toDart;
|
||||
}
|
||||
|
||||
@override
|
||||
String get permission => web.Notification.permission.toString();
|
||||
|
|
@ -25,32 +28,18 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
return result.toDart;
|
||||
}
|
||||
|
||||
// ── display / click bridge ──────────────────────────────────────
|
||||
// -- display / click bridge --
|
||||
|
||||
/// Captured click callback set via [setClickHandler].
|
||||
void Function(Map<String, Object?>)? _clickHandler;
|
||||
|
||||
/// Routing data captured at the moment the notification was displayed
|
||||
/// so that the click event listener can reconstruct it.
|
||||
Map<String, Object?> _lastRoutingData = const <String, Object?>{};
|
||||
|
||||
@override
|
||||
void setClickHandler(void Function(Map<String, Object?> data) onClick) {
|
||||
_clickHandler = onClick;
|
||||
// Register the click event listener once using window.
|
||||
_registerClick();
|
||||
}
|
||||
|
||||
void _registerClick() {
|
||||
// Click handler is stored in [_clickHandler]. In a real browser environment,
|
||||
// the notificationclick event would be dispatched by the browser when the
|
||||
// user clicks a notification. The host app wires this up via
|
||||
// Service Worker or window.addEventListener('notificationclick', ...) in
|
||||
// consuming application code.
|
||||
//
|
||||
// For testing purposes, setClickHandler stores the callback so unit tests
|
||||
// can invoke it directly via [invokeClick] (visible for testing).
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void invokeClick(Map<String, Object?> data) {
|
||||
_clickHandler?.call(data);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -58,26 +47,63 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
if (!isSupported) return false;
|
||||
try {
|
||||
final title = options['title'] as String?;
|
||||
if (title == null) {
|
||||
return false;
|
||||
if (title == null) return false;
|
||||
|
||||
// Capture routing fields for the click handler.
|
||||
_lastRoutingData = <String, Object?>{};
|
||||
final routingKeys = <String>[
|
||||
'server_url',
|
||||
'channel_id',
|
||||
'root_id',
|
||||
'is_crt_enabled',
|
||||
];
|
||||
for (final key in routingKeys) {
|
||||
final val = options[key];
|
||||
if (val != null) {
|
||||
_lastRoutingData[key] = val;
|
||||
}
|
||||
}
|
||||
final opts = _buildNotificationOptions(options);
|
||||
web.Notification(title, opts);
|
||||
|
||||
// Build notification options (title, body, tag, icon).
|
||||
final body = options['body'] as String? ?? '';
|
||||
final icon = options['icon'] as String? ?? '';
|
||||
final tag = options['tag'] as String? ?? '';
|
||||
|
||||
final opts = web.NotificationOptions(
|
||||
body: body,
|
||||
icon: icon,
|
||||
tag: tag,
|
||||
);
|
||||
final notification = web.Notification(title, opts);
|
||||
|
||||
// Attach the click handler immediately after creation.
|
||||
_attachClickHandler(notification);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a [web.NotificationOptions] from the options map.
|
||||
web.NotificationOptions _buildNotificationOptions(Map<String, Object?> options) {
|
||||
final body = options['body'] as String?;
|
||||
final icon = options['icon'] as String?;
|
||||
final tag = options['tag'] as String?;
|
||||
return web.NotificationOptions(
|
||||
body: body ?? '',
|
||||
icon: icon ?? '',
|
||||
tag: tag ?? '',
|
||||
);
|
||||
/// Attaches a click listener on [notification] that forwards the
|
||||
/// stored routing data to the [_clickHandler] callback.
|
||||
void _attachClickHandler(web.Notification notification) {
|
||||
final data = _lastRoutingData;
|
||||
final clickListener = ((web.Event event) {
|
||||
_clickHandler?.call(data);
|
||||
}).toJS;
|
||||
try {
|
||||
notification.addEventListener(
|
||||
'click',
|
||||
clickListener,
|
||||
false.toJS,
|
||||
);
|
||||
} catch (_) {
|
||||
// Best-effort: failure here will not crash the notification display.
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void invokeClick(Map<String, Object?> data) {
|
||||
_clickHandler?.call(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue