feat(messaging): Flutter Web 알림 마일스톤을 완료 상태로 승격하고 알림 라우팅을 개선한다
- PHASE.md에서 flutter-web-notification-smoke 마일스톤 상태를 완료로 변경하고 archive로 이동 - browser_notification_interop_web.dart에서 라우팅 데이터 캡처 방식을 인스턴스 변수에서 인자 전달로 변경하여 각 알림별 데이터 격리 - nexo_messaging_plugin.dart에서 permissionGranted를 고정 값에서 실제 권한 상태 기반으로 변경 - 코드 스타일 개선: if문 블록 포맷팅, 단일 줄 최적화 - 테스트 추가: show 실패 시 최신 권한 상태를 반환하는 검증
This commit is contained in:
parent
f3bba0a1ca
commit
079c2984bd
5 changed files with 69 additions and 39 deletions
|
|
@ -12,7 +12,7 @@ Flutter Web foreground browser notification이 실제 브라우저 환경에서
|
|||
|
||||
## 상태
|
||||
|
||||
[검토중]
|
||||
[완료]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -54,17 +54,18 @@ VM 단위 테스트와 Chrome/web 테스트가 서로 다른 platform expectatio
|
|||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 요청됨
|
||||
- 상태: 승인됨
|
||||
- 요청일: 2026-06-08
|
||||
- 승인일: 2026-06-09
|
||||
- 완료 근거:
|
||||
- `agent-task/archive/2026/06/m-flutter-web-notification-smoke/01_web_scaffold/complete.log`
|
||||
- `agent-task/archive/2026/06/m-flutter-web-notification-smoke/02+01_web_runtime/complete.log`
|
||||
- `agent-task/archive/2026/06/m-flutter-web-notification-smoke/03_chrome_tests/complete.log`
|
||||
- `agent-task/archive/2026/06/m-flutter-web-notification-smoke/04+03_manual_evidence_closeout/complete.log`
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 2026-06-09 코드 관점 재검토에서 foreground web notification permission result와 notification별 click routing data 보강 후 `flutter analyze --no-fatal-infos`, `flutter test`, Chrome web test, 기본/opt-in `bin/test`가 통과했다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -40,8 +40,8 @@ Mattermost server/webapp/push-proxy는 upstream-followable runtime으로 두고,
|
|||
- [완료] Flutter Web 알림
|
||||
- 경로: `agent-roadmap/archive/phase/messaging-runtime/milestones/flutter-web-notification.md`
|
||||
- 요약: `packages/messaging_flutter`를 Flutter Web/Chrome foreground browser notification까지 확장하고, Full Web Push는 별도 후속 계약으로 분리한다.
|
||||
- [검토중] Flutter Web 알림 Smoke 보완
|
||||
- 경로: `agent-roadmap/phase/messaging-runtime/milestones/flutter-web-notification-smoke.md`
|
||||
- [완료] Flutter Web 알림 Smoke 보완
|
||||
- 경로: `agent-roadmap/archive/phase/messaging-runtime/milestones/flutter-web-notification-smoke.md`
|
||||
- 요약: Flutter Web foreground browser notification smoke가 원격 web runner와 forwarded localhost URL에서 실제로 재현되도록 host scaffold, Chrome 테스트, 수동 evidence 기준을 보완한다.
|
||||
- [계획] iOS 알림 테스트
|
||||
- 경로: `agent-roadmap/phase/messaging-runtime/milestones/ios-notification-test.md`
|
||||
|
|
|
|||
|
|
@ -206,10 +206,18 @@ class NexoMessagingPlugin {
|
|||
}
|
||||
|
||||
// Forward routing fields so click callback can reconstruct the event.
|
||||
if (payload['server_url'] != null) options['server_url'] = payload['server_url'];
|
||||
if (payload['channel_id'] != null) options['channel_id'] = payload['channel_id'];
|
||||
if (payload['root_id'] != null) options['root_id'] = payload['root_id'];
|
||||
if (payload['is_crt_enabled'] != null) options['is_crt_enabled'] = payload['is_crt_enabled'];
|
||||
if (payload['server_url'] != null) {
|
||||
options['server_url'] = payload['server_url'];
|
||||
}
|
||||
if (payload['channel_id'] != null) {
|
||||
options['channel_id'] = payload['channel_id'];
|
||||
}
|
||||
if (payload['root_id'] != null) {
|
||||
options['root_id'] = payload['root_id'];
|
||||
}
|
||||
if (payload['is_crt_enabled'] != null) {
|
||||
options['is_crt_enabled'] = payload['is_crt_enabled'];
|
||||
}
|
||||
|
||||
// Show the notification; set up click routing once on first call.
|
||||
if (!_clickHandlerRegistered) {
|
||||
|
|
@ -218,6 +226,8 @@ class NexoMessagingPlugin {
|
|||
}
|
||||
|
||||
final success = _interop.show(options);
|
||||
final permissionGranted = _interop.permission.toLowerCase() == 'granted';
|
||||
|
||||
if (success) {
|
||||
// Forward the raw notification to onNotification stream.
|
||||
final normalized = normalizeWebForegroundMessage(payload);
|
||||
|
|
@ -229,7 +239,7 @@ class NexoMessagingPlugin {
|
|||
return NexoMessagingWebNotificationDisplayResult(
|
||||
success: success,
|
||||
isSupported: true,
|
||||
permissionGranted: true,
|
||||
permissionGranted: permissionGranted,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,6 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
/// 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;
|
||||
|
|
@ -49,8 +45,8 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
final title = options['title'] as String?;
|
||||
if (title == null) return false;
|
||||
|
||||
// Capture routing fields for the click handler.
|
||||
_lastRoutingData = <String, Object?>{};
|
||||
// Capture routing fields for this notification.
|
||||
final routingData = <String, Object?>{};
|
||||
final routingKeys = <String>[
|
||||
'server_url',
|
||||
'channel_id',
|
||||
|
|
@ -60,7 +56,7 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
for (final key in routingKeys) {
|
||||
final val = options[key];
|
||||
if (val != null) {
|
||||
_lastRoutingData[key] = val;
|
||||
routingData[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,15 +65,11 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
final icon = options['icon'] as String? ?? '';
|
||||
final tag = options['tag'] as String? ?? '';
|
||||
|
||||
final opts = web.NotificationOptions(
|
||||
body: body,
|
||||
icon: icon,
|
||||
tag: tag,
|
||||
);
|
||||
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);
|
||||
_attachClickHandler(notification, routingData);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
|
|
@ -86,17 +78,15 @@ class BrowserNotificationInteropImpl extends BrowserNotificationInterop {
|
|||
|
||||
/// Attaches a click listener on [notification] that forwards the
|
||||
/// stored routing data to the [_clickHandler] callback.
|
||||
void _attachClickHandler(web.Notification notification) {
|
||||
final data = _lastRoutingData;
|
||||
void _attachClickHandler(
|
||||
web.Notification notification,
|
||||
Map<String, Object?> routingData,
|
||||
) {
|
||||
final clickListener = ((web.Event event) {
|
||||
_clickHandler?.call(data);
|
||||
_clickHandler?.call(routingData);
|
||||
}).toJS;
|
||||
try {
|
||||
notification.addEventListener(
|
||||
'click',
|
||||
clickListener,
|
||||
false.toJS,
|
||||
);
|
||||
notification.addEventListener('click', clickListener, false.toJS);
|
||||
} catch (_) {
|
||||
// Best-effort: failure here will not crash the notification display.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,18 @@ import 'package:nexo_messaging/src/web/foreground_message_mapper.dart';
|
|||
class FakeBrowserNotificationInterop implements BrowserNotificationInterop {
|
||||
FakeBrowserNotificationInterop({
|
||||
this.isSupportedParam = true,
|
||||
this.permissionParam = 'granted',
|
||||
String permissionParam = 'granted',
|
||||
this.showReturnValue = true,
|
||||
String? permissionAfterShow,
|
||||
}) : _isSupported = isSupportedParam,
|
||||
_permission = permissionParam;
|
||||
_permission = permissionParam,
|
||||
_permissionAfterShow = permissionAfterShow;
|
||||
|
||||
final bool _isSupported;
|
||||
final String _permission;
|
||||
String _permission;
|
||||
final String? _permissionAfterShow;
|
||||
final bool isSupportedParam;
|
||||
final String permissionParam;
|
||||
final bool showReturnValue;
|
||||
|
||||
bool showCalled = false;
|
||||
Map<String, Object?>? lastShowOptions;
|
||||
|
|
@ -33,7 +37,10 @@ class FakeBrowserNotificationInterop implements BrowserNotificationInterop {
|
|||
if (!isSupported) return false;
|
||||
showCalled = true;
|
||||
lastShowOptions = Map<String, Object?>.from(options);
|
||||
return true;
|
||||
if (_permissionAfterShow != null) {
|
||||
_permission = _permissionAfterShow;
|
||||
}
|
||||
return showReturnValue;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -563,13 +570,20 @@ void main() {
|
|||
late NexoMessagingPlugin plugin;
|
||||
late FakeBrowserNotificationInterop fake;
|
||||
|
||||
void setUpWith({bool isSupported = true, String perm = 'granted'}) {
|
||||
void setUpWith({
|
||||
bool isSupported = true,
|
||||
String perm = 'granted',
|
||||
bool showReturnValue = true,
|
||||
String? permissionAfterShow,
|
||||
}) {
|
||||
final ac = MethodChannel(NexoMessagingPlugin.actionChannelName);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(ac, (call) async => null);
|
||||
fake = FakeBrowserNotificationInterop(
|
||||
isSupportedParam: isSupported,
|
||||
permissionParam: perm,
|
||||
showReturnValue: showReturnValue,
|
||||
permissionAfterShow: permissionAfterShow,
|
||||
);
|
||||
plugin = NexoMessagingPlugin.instance;
|
||||
plugin.resetForTesting();
|
||||
|
|
@ -615,6 +629,21 @@ void main() {
|
|||
expect(fake.lastShowOptions!['body'], 'B');
|
||||
});
|
||||
|
||||
test('show failure reads latest permission before returning', () {
|
||||
setUpWith(
|
||||
isSupported: true,
|
||||
perm: 'granted',
|
||||
showReturnValue: false,
|
||||
permissionAfterShow: 'denied',
|
||||
);
|
||||
|
||||
final r = plugin.showWebForegroundNotification({'title': 'H'});
|
||||
|
||||
expect(r.success, isFalse);
|
||||
expect(r.isSupported, isTrue);
|
||||
expect(r.permissionGranted, isFalse);
|
||||
});
|
||||
|
||||
test('interop.show() receives routing fields', () {
|
||||
setUpWith();
|
||||
plugin.showWebForegroundNotification({
|
||||
|
|
|
|||
Loading…
Reference in a new issue