nexo/packages/messaging_flutter
toki 079c2984bd feat(messaging): Flutter Web 알림 마일스톤을 완료 상태로 승격하고 알림 라우팅을 개선한다
- PHASE.md에서 flutter-web-notification-smoke 마일스톤 상태를 완료로 변경하고 archive로 이동
- browser_notification_interop_web.dart에서 라우팅 데이터 캡처 방식을 인스턴스 변수에서 인자 전달로 변경하여 각 알림별 데이터 격리
- nexo_messaging_plugin.dart에서 permissionGranted를 고정 값에서 실제 권한 상태 기반으로 변경
- 코드 스타일 개선: if문 블록 포맷팅, 단일 줄 최적화
- 테스트 추가: show 실패 시 최신 권한 상태를 반환하는 검증
2026-06-10 20:40:17 +09:00
..
android feat(m-notification-pipeline): archive ack_actions & host_routing_contract tasks, add PendingNotificationActionStore 2026-05-31 09:36:41 +09:00
docs feat: client-validation task and update READMEs 2026-05-28 20:08:15 +09:00
ios feat: update iOS notification test and plugin implementation 2026-06-01 13:38:32 +09:00
lib feat(messaging): Flutter Web 알림 마일스톤을 완료 상태로 승격하고 알림 라우팅을 개선한다 2026-06-10 20:40:17 +09:00
macos refactor: rename mattermost_push_plugin to nexo_messaging package and update branding 2026-05-26 20:19:04 +09:00
test feat(messaging): Flutter Web 알림 마일스톤을 완료 상태로 승격하고 알림 라우팅을 개선한다 2026-06-10 20:40:17 +09:00
.gitignore Initial nexo monorepo migration 2026-05-26 10:42:29 +09:00
.metadata Initial nexo monorepo migration 2026-05-26 10:42:29 +09:00
analysis_options.yaml Initial nexo monorepo migration 2026-05-26 10:42:29 +09:00
CHANGELOG.md Initial nexo monorepo migration 2026-05-26 10:42:29 +09:00
LICENSE Initial nexo monorepo migration 2026-05-26 10:42:29 +09:00
pubspec.yaml feat(messaging): Web 플러그인 기준선을 추가한다 2026-06-07 12:55:56 +09:00
README.md fix(messaging): Web 알림 클릭 라우팅을 연결한다 2026-06-07 19:04:08 +09:00

nexo_messaging

Android-first Flutter plugin for the nexo messaging and push-notification runtime.

The plugin owns the push-notification domain that should not be duplicated in each consuming app: native FCM handling, notification display, notification tap events, inline replies, dismiss handling, ACK delivery, device-token storage, auth-token storage, and server signing-key storage.

SDK Ownership Boundary

This package is the nexo-owned embedded messaging SDK. Its Android implementation follows the nexo server/push contract, Android and FCM platform changes, and Flutter plugin registration behavior. It does not follow or mirror the Mattermost mobile app source tree; that app remains under apps/mattermost/ as an upstream clone.

Runtime Tracking Boundary

The SDK tracks the server/push contract fields required for embedded messaging: push payload routing fields, ACK/reply endpoints, signing-key and device-token compatibility, Android/FCM delivery behavior, and Flutter plugin registration. The Android runtime may keep compatibility with Mattermost server and push-proxy payload conventions where those contracts remain part of nexo messaging.

The SDK does not track Mattermost mobile implementation details such as React Native screens, stores, navigation state, product UI, or app-specific business logic. Host apps own their own screens and routers. Future typed notification wrappers should be added as nexo SDK API after the raw event-shape contract is stable; they are not a reason to mirror the Mattermost mobile source tree.

Platform-follow criteria define what a platform change review must re-check instead of merging Mattermost mobile source. Track and re-verify against:

  • Server/push payload changes: the fields in Push Payload Fields and the ACK Contract request/response shape.
  • FCM delivery changes: how NexoFirebaseMessagingService is woken, the data payload shape, and signature/ACK ordering.
  • Android notification platform changes: notification permission, channel configuration, group/summary decision, inline reply, and dismiss behavior.

When any of those shift, the platform-follow review updates the relevant contract section and the regression tests under android/src/test/ rather than copying upstream mobile code.

Platform Status

Platform Status Notes
Android Production target Native FCM service, Room storage, JJWT verification, OkHttp ACK client, notification builder, inline reply receiver, dismiss receiver
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
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
Web Foreground display Plugin registration (NexoMessagingWeb.registerWith()), package:web dependency, browser Notification interop boundary (BrowserNotificationInterop), foreground payload mapper (normalizeWebForegroundMessage), and public display API (showWebForegroundNotification) are in place. Click events are wired to handleNativeEvent()onNotificationOpened/onNavigateToChannel/onNavigateToThread callbacks. Full Web Push (service worker, VAPID, server device-token registration) follows.

Repository Layout

packages/messaging_flutter/
  lib/                    # Public Dart API and Flutter-facing behavior
  android/                # Android native plugin implementation
  ios/                    # iOS plugin scaffold
  macos/                  # macOS plugin scaffold
  # windows/              # Not registered yet; see Windows preflight docs
  test/                   # Dart unit tests for the plugin contract
  ../../apps/flutter-test/      # Flutter test host

The workspace apps/flutter-test/ app validates plugin integration without turning this repository into a product-app workspace. The original Mattermost app belongs under apps/mattermost/, not in the test host.

How It Works

graph TD
    A[nexo Server] -->|Secure JWT Push| B(Android FCM Service)
    B -->|Verify signing key| C{Valid?}
    C -->|Yes| D[Store payload and build notification]
    C -->|No| E[Drop push]
    D -->|Send ACK| A
    D -->|Display notification| F(Android status bar)
    F -->|Tap| G[Flutter opened event]
    F -->|Inline reply| H[Reply receiver]
    H -->|Send reply| A
    G -->|Stream and callbacks| I(Host app navigation)

Host App Responsibilities

The plugin isolates push handling, but the host application still owns app-level integration:

  1. Configure Firebase for the host app, including google-services.json under android/app/.
  2. Initialize Firebase as required by the host app before push registration.
  3. Call NexoMessagingPlugin.instance.initialize() during app startup.
  4. Save auth tokens and signing keys after login.
  5. Clear auth tokens on logout.
  6. Send the formatted device token to the nexo server.
  7. Connect notification-open callbacks to the host app router.

Host Integration Contract

Host apps must keep product navigation and login state outside the plugin while calling the SDK hooks at stable lifecycle points:

  • Startup: initialize Firebase for the host app, then call NexoMessagingPlugin.instance.initialize().
  • Login or session restore: call setAuthToken(serverUrl, token, identifier: optionalServerId) and setSigningKey(serverUrl, signingKey) when those values become available.
  • Device token handoff: send values received from onDeviceTokenReady or getDeviceToken() to the nexo server; host apps should not parse the token prefix as app state.
  • Routing: subscribe to onNotificationOpened and/or set onNavigateToChannel and onNavigateToThread to bridge notification opens to host-owned screens.
  • Logout: call clearAuthToken(serverUrl) and clear host-owned routing/session state. Device-token storage and signing-key storage are plugin compatibility state and are not product UI state.

True FCM delivery, server ACK/reply responses, inline reply, and dismiss action evidence remain manual smoke checks because they require external Firebase, server, and device state.

Installation

For local development or another app in the same workspace, use a path dependency:

dependencies:
  nexo_messaging:
    path: ../../packages/messaging_flutter

For a private Git dependency, point the consuming app at the repository and pin the branch, tag, or commit according to your release process.

Dart Usage

Import the public API:

import 'package:nexo_messaging/nexo_messaging.dart';

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:

await NexoMessagingPlugin.instance.initialize(
  options: const NexoMessagingInitializeOptions(
    listenForTokenRefresh: true,
    requestNotificationPermission: true,
    registerDeviceToken: true,
  ),
);

Listen to raw notification events:

NexoMessagingPlugin.instance.onNotification.listen((data) {
  // data includes the native payload plus a push type.
});

Listen to notification-open events:

NexoMessagingPlugin.instance.onNotificationOpened.listen((event) {
  final serverUrl = event.serverUrl;
  final channelId = event.channelId;
  final rootId = event.rootId;
});

Register navigation callbacks:

NexoMessagingPlugin.instance.onNavigateToChannel = (
  String serverUrl,
  String channelId,
) {
  // Navigate to the channel in the host app.
};

NexoMessagingPlugin.instance.onNavigateToThread = (
  String serverUrl,
  String rootId,
) {
  // Navigate to the CRT thread in the host app.
};

Handle device-token registration:

NexoMessagingPlugin.instance.onDeviceTokenReady = (String deviceToken) {
  // 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.
};

Store credentials and signing keys:

await NexoMessagingPlugin.instance.setAuthToken(
  'https://nexo.example.com',
  'USER_AUTH_TOKEN',
  identifier: 'user_session_id',
);

await NexoMessagingPlugin.instance.setSigningKey(
  'https://nexo.example.com',
  'SERVER_PUBLIC_SIGNING_KEY',
);

await NexoMessagingPlugin.instance.clearAuthToken(
  'https://nexo.example.com',
);

final token = await NexoMessagingPlugin.instance.getDeviceToken();

Web Foreground Notifications

On Flutter Web, the plugin can display browser notifications and route clicks to the same Dart streams as native events:

// Request permission first.
final status = await NexoMessagingPlugin.instance
    .requestWebNotificationPermission();
if (status.isGranted) {
  // Display a foreground notification.
  final result = NexoMessagingPlugin.instance.showWebForegroundNotification({
    'title': 'New message',
    'body': 'Hello from Nexo Messaging',
    'server_url': 'https://nexo.example.com',
    'channel_id': 'channel-abc',
    'root_id': 'root-123',
    'is_crt_enabled': 'true',
  });
  // result.success == true means the browser displayed it.
  // Click events are automatically routed through onNotificationOpened
  // and the onNavigateToChannel / onNavigateToThread callbacks.
}

Token And Credential Flow

The host app owns login, logout, and server registration. The SDK owns local native storage and native push operations that need those values.

  1. The host app configures Firebase, initializes Firebase for its app, and calls 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. 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. 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.
  5. After login, the host app calls setAuthToken(serverUrl, token, identifier: optionalServerId). Native code stores the HTTP token for ACK/reply requests and stores the server URL with the optional identifier so push payloads containing server_id can be resolved to a server URL.
  6. The host app calls setSigningKey(serverUrl, signingKey) when the server signing key is available. Native verification first checks the per-server DB config signing key, then falls back to the signing key stored through this API.
  7. On logout, clearAuthToken(serverUrl) clears the native HTTP token used for ACK/reply delivery. It is not a server URL mapping deletion API and does not clear the stored device token or signing key.

If a push payload omits server_url, Android may resolve it from server_id using the identifier saved by setAuthToken, or from the only stored server URL 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.

02+01_web_plugin_baseline has delivered:

  • Flutter web plugin registration (NexoMessagingWeb.registerWith()).
  • package:web: ^1.1.1 as a dependency.
  • A web-only browser Notification interop boundary (BrowserNotificationInterop / BrowserNotificationInteropImpl) exposing permission and requestPermission().
  • lib/src/web/ imports are confined to web-only files; common Dart code does not import package:web.

The foreground Epic has delivered:

  • Public display / click routing APIs (showWebForegroundNotification).
  • Click-to-open routing wired through handleNativeEvent()onNotificationOpened / onNavigateToChannel / onNavigateToThread.
  • Browser notification display with permission gating (unsupported/denied/granted).

Foreground browser permission APIs are available on the plugin singleton:

  • getWebNotificationPermissionStatus() returns support and current browser permission state.
  • requestWebNotificationPermission() requests browser notification permission on Flutter Web and returns the resulting state. On non-web platforms it returns an unsupported status without touching native permission APIs.

Public API Contract

Apps should import only package:nexo_messaging/nexo_messaging.dart. The exported Dart surface is the nexo SDK contract; method-channel names, native payload internals, and token formatting remain compatibility details unless they are listed here.

Surface Status Contract
NexoMessagingPlugin.instance Keep Singleton entry point for host app startup, event subscription, credential storage, and token lookup.
initialize() / dispose() Keep Plugin lifecycle hooks. Host apps call initialize() during startup and dispose() only when intentionally tearing down the SDK instance.
onNotification Keep, compatibility Raw native event stream for advanced diagnostics and compatibility. Host apps should avoid product routing decisions that depend on untyped map fields.
onNotificationOpened Keep Typed stream for notification-open routing through NotificationOpenedEvent.
onNavigateToChannel / onNavigateToThread Keep Optional host-router callbacks. Screen ownership and product navigation remain outside this package.
onDeviceTokenReady / getDeviceToken() Keep Device-token handoff to the host app and server registration flow. The android_rn-v2: prefix is a server compatibility detail, not an app parsing contract.
setAuthToken() / clearAuthToken() Keep Auth-token persistence bridge for native ACK and reply delivery.
setSigningKey() Keep Server signing-key persistence bridge for native push verification.
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.
showWebForegroundNotification() New Displays a foreground notification via the browser Notification API. Returns NexoMessagingWebNotificationDisplayResult. On non-web or denied platforms it returns unsupported/no-op.
NexoMessagingWebNotificationDisplayResult New Result value object for showWebForegroundNotification(). Contains success, isSupported, and permissionGranted fields.

Deprecated candidates are compatibility-only app dependencies on raw onNotification maps, direct branching on PushNotificationType, and parsing the formatted device-token prefix. Nexo wrapper candidates are typed notification-event models and a token-registration facade, to be finalized after the event-shape and token-flow contracts are documented.

Event Shape Contract

Native events are delivered to Dart as codec-safe maps on onNotification. Apps may log additional native fields for diagnostics, but app behavior should only depend on the stable fields listed here or on typed SDK objects such as NotificationOpenedEvent.

Event Required fields Optional stable fields Compatibility
opened type server_url, channel_id, root_id, is_crt_enabled, userInteraction Launch intents with missing native type are treated as opened; Android adds userInteraction: true for intent-opened payloads.
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 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, the string "true", and the string "1" as CRT-enabled. If CRT is enabled and root_id is present, the plugin calls onNavigateToThread; otherwise it calls onNavigateToChannel when channel_id and server_url are available.

Event Types

The Dart API currently recognizes these native event types:

Type Meaning
message A nexo message notification
clear Notification clear or dismiss event
session Session-related event
token_refresh Device-token refresh event
opened User opened a notification
action_failure Permanent native ACK or reply action failure forwarded for host diagnostics

NotificationOpenedEvent parses server_url, channel_id, root_id, and is_crt_enabled. If CRT is enabled and root_id is present, the plugin calls onNavigateToThread; otherwise it calls onNavigateToChannel when a channel is available.

Server And Push Runtime Contract

This package consumes the Mattermost-compatible push runtime documented from the server side in services/core/compose/README.md. The plugin keeps compatibility with those fields, while typed app-facing API stays in the Dart surface above.

Push Payload Fields

Server-side source anchor: services/core/server/public/model/push_notification.go.

Plugin-side source anchors: android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoFirebaseMessagingService.kt, android/src/main/java/com/tokilabs/nexo/messaging/helpers/CustomPushNotificationHelper.java, and android/src/main/kotlin/com/tokilabs/nexo/messaging/LaunchIntentHelper.kt.

Field Required for Plugin handling
type Message, clear, session dispatch Must match the native PushNotificationType constants. Unknown types are forwarded only as raw compatibility events.
ack_id ACK and signature validation Sent back to /api/v4/notifications/ack and required by signed push JWT validation when a signature is present.
server_url ACK/reply delivery, signature lookup, routing Preferred server identity. If omitted, Android may resolve it from server_id or the single stored server URL.
server_id Server URL fallback Resolved through the identifier stored by setAuthToken(serverUrl, token, identifier: ...).
channel_id Notification grouping and channel routing Used for notification grouping, clear handling, and onNavigateToChannel.
root_id CRT/thread routing When CRT is enabled, this groups thread notifications and drives onNavigateToThread.
is_crt_enabled Thread behavior Boolean true, string "true", or string "1" enables thread routing; other values are treated as false.
post_id ACK id-loaded response and notification identity Included in ACK requests and used as the Android notification id source.
signature Payload verification Verified against the server signing key and stored device token. Missing signatures are accepted for backward compatibility.
id_loaded ACK response enrichment The string value "true" asks the server to return post data that Android can merge into the notification bundle.
category Inline reply affordance CAN_REPLY enables the Android inline reply action when post_id and server_url are present.

Validation criteria:

  • A signed payload passes only when the JWT contains the expected ack_id and raw device token, and the signing key resolves from the server database or the setSigningKey() fallback.
  • Signature verification runs before the ACK. An invalid signature payload is dropped without being acknowledged and without rendering a notification.
  • A payload without server_url is valid only when server_id or the one-server fallback resolves to a server URL before ACK, reply, or signature lookup.
  • Payload fields not listed here may pass through raw onNotification maps for diagnostics, but consuming apps should not treat them as stable API.

Display criteria:

  • A notification groups by root_id when CRT is enabled and a root_id is present, otherwise by channel_id; a payload without channel_id is rendered ungrouped.
  • The first notification in a group is posted as the group summary; later notifications in the same group are not.
  • The channel_id, post_id, root_id, is_crt_enabled, and server_url fields are preserved into the notification userInfo extras so the opened event can recover channel and thread identity.

ACK Contract

Plugin-side source anchor: android/src/main/java/com/tokilabs/nexo/messaging/ReceiptDelivery.java.

Server-side source anchors: services/core/server/public/model/push_notification.go and services/core/server/channels/api4/system.go.

The plugin POSTs ACKs to /api/v4/notifications/ack with:

Request field Value
id Push payload ack_id.
received_at Client receipt timestamp in milliseconds.
platform android.
type Push payload type.
post_id Push payload post_id, when present.
is_id_loaded Boolean derived from payload id_loaded == "true".

The server returns OK for ordinary ACKs. When is_id_loaded is true and the ACK references a message post, the server may return message fields such as post_id, root_id, category, message, team_id, channel_id, channel_name, type, sender_id, sender_name, and version; Android merges those fields into the notification bundle before rendering.

Failure handling criteria:

  • Signature verification precedes ACK delivery, so an invalid signature payload never reaches the ACK request.
  • If ACK delivery fails for an already-verified payload because of an offline, timeout, 408, 429, or 5xx condition, Android stores a local pending action and retries it opportunistically with a bounded three-attempt limit. Notification processing continues with the original payload.
  • If an id-loaded ACK response cannot be parsed, Android treats it as absent and continues with the original payload.
  • If the server rejects the ACK because push notifications are disabled, unauthenticated, or the post is unavailable, Android treats it as permanent, removes any pending action, and forwards an action_failure native event for host diagnostics. This is a runtime/server state failure, not a Dart API contract change.

Reply And Dismiss Contract

Plugin-side source anchors: android/src/main/java/com/tokilabs/nexo/messaging/NotificationReplyBroadcastReceiver.java and android/src/main/java/com/tokilabs/nexo/messaging/NotificationDismissService.java.

Inline replies are posted to /api/v4/posts?set_online=false using the server URL and auth token stored by setAuthToken().

Failure handling criteria:

  • A successful 2xx reply updates the Android notification with the sent message.
  • Offline, timeout, 408, 429, and 5xx reply failures are stored as local pending actions and retried opportunistically with the same three-attempt bound as ACK delivery.
  • 401, 403, and other non-retryable 4xx reply failures are permanent. Android removes the pending action if present and forwards action_failure for host diagnostics.
  • Dismiss has no server endpoint in the current nexo contract. The plugin cancels local notification state only and does not invent a network request or failure telemetry for a successful local dismiss.

Reference Front Compatibility

The Mattermost webapp under services/core/webapp remains the reference front for browser messaging features such as posts, replies, post acknowledgements, channel navigation, and thread state. The Flutter SDK embedded flow must not replace or fork that webapp behavior inside this repository.

Compatibility criteria:

  • Webapp routes, Redux state, and product UI stay upstream-followable in services/core/webapp.
  • Flutter consuming apps own their own screens and routers; this plugin emits opened events and optional channel/thread callbacks only.
  • Server compatibility belongs to push payload fields, ACK/reply endpoints, signing-key verification, and device-token format, not to Mattermost mobile or webapp UI implementation details.
  • If a future Flutter app needs product UI parity with the reference webapp, that work belongs in the consuming app or a separate roadmap item, not in this plugin contract milestone.

Android Notes

The Android manifest declares the plugin's own NexoFirebaseMessagingService and removes the default FlutterFire messaging components with tools:node="remove". This avoids duplicate FCM service registration when the host app also depends on firebase_messaging.

Host apps should not redeclare duplicate FCM services unless they intentionally coordinate that behavior with this plugin.

Android Gradle tests require a configured Android SDK through ANDROID_HOME or apps/flutter-test/android/local.properties.

Development Commands

Run Dart analysis and unit tests from the repository root:

flutter analyze
flutter test

Run flutter-test checks from the app path:

cd apps/flutter-test
flutter analyze
flutter test
flutter test integration_test

Run Android native unit tests:

cd apps/flutter-test/android
./gradlew testDebugUnitTest

If the local machine does not have an Android SDK or Android device/emulator, use the separate SSH-based Android test environment documented in docs/android-test-environment.md.

macOS Preflight Notes

macOS remains a scaffold, but its preflight contract is fixed before native implementation work starts. Dart expects the macOS plugin to register com.tokilabs.nexo.messaging/notifications and com.tokilabs.nexo.messaging/notification_actions, preserve the legacy nexo_messaging getPlatformVersion compatibility channel, and map notification userInfo fields to NotificationOpenedEvent.

Use ../../docs/macos-notification-test-guide.md to identify the Apple signing, macOS runner, APNs/Firebase scope, and private handoff values the user must provide before an agent can complete macOS notification smoke work.

Windows Preflight Notes

Windows is not registered in pubspec.yaml and has no plugin scaffold yet. Do not add a partial Windows platform entry without a matching implementation plan that creates the plugin source, test host scaffold, channel registration, and debug injection path together.

Dart still expects any future Windows bridge to use com.tokilabs.nexo.messaging/notifications and com.tokilabs.nexo.messaging/notification_actions, then map toast activation or debug injection payloads to NotificationOpenedEvent.

Use ../../docs/windows-notification-test-guide.md to identify the Windows runner, packaging mode, notification API path, package identity, signing, and private handoff values the user must provide before an agent can complete Windows notification smoke work.

Test Strategy

Use deterministic tests for repository-owned behavior. The example integration test can inject a debug native event through the plugin method channel so the EventChannel and opened-routing path can run without Firebase FCM.

The Android implementation remains a nexo-owned embedded SDK surface in these tests. Test coverage should follow the server/push contract, Android and FCM platform behavior, and Flutter plugin registration rather than the Mattermost mobile app source tree.

Use the repository tests as the source of truth for plugin behavior:

  1. Root test/ covers Dart API behavior, event parsing, callback routing, and device-token formatting.
  2. android/src/test/ should cover native payload parsing, signing-key lookup, local storage, notification behavior, and ACK request behavior.
  3. apps/flutter-test/test/ covers the thin Flutter test app.
  4. apps/flutter-test/integration_test/ covers real plugin registration, method-channel behavior, event-channel behavior, notification-open routing, and manifest integration on a device or emulator using the test/debug injection path.
  5. Manual smoke tests cover true Firebase FCM delivery and real nexo server ACK flows because they depend on external infrastructure.

Manual Smoke Checklist

Use this checklist before promoting a plugin change to a consuming app. These checks require external Firebase and nexo server infrastructure, so they are not expected to pass in a headless repository test run.

Prerequisites:

  • Firebase is configured for the host app with android/app/google-services.json.
  • The host app has called NexoMessagingPlugin.instance.initialize().
  • The test user has a server URL, auth token, and signing key saved through the plugin API.
  • The nexo server can send a push payload with ack_id, server_url or resolvable server_id, channel_id, and a valid signature.

Evidence to capture:

  • logcat lines from NexoFirebaseMessagingService for receipt, ACK, and signature handling.

  • A screenshot or device recording of the displayed notification.

  • Server-side ACK/reply request logs showing response status.

  • Host app UI evidence for onNotificationOpened, onNavigateToChannel, and onNavigateToThread routing.

  • FCM payload wakes NexoFirebaseMessagingService.

  • Valid nexo push payload passes signing-key verification.

  • Invalid signature payload is dropped before ACK (no ACK request is sent and no notification is shown).

  • Unsigned payload is accepted for backward compatibility.

  • First notification in a channel/thread group shows the group summary and later notifications in the same group do not.

  • System notification displays the expected title, message, avatar, and CRT threading behavior.

  • ACK request is sent to the nexo server and receives a successful response.

  • Tapping a notification launches or resumes the app and emits onNotificationOpened.

  • Channel notifications call onNavigateToChannel.

  • CRT thread notifications call onNavigateToThread.

  • Inline reply action sends the reply through the native receiver.

  • Dismiss action clears native notification records as expected.

  • Device token is stored with the android_rn-v2: prefix.

Current Limitations

  • Android is the only implemented runtime target.
  • iOS and macOS are scaffolded but no-op.
  • Web baseline (NexoMessagingWeb.registerWith(), BrowserNotificationInterop, package:web dependency) is implemented, and the foreground display API (showWebForegroundNotification) with click routing is implemented.
    • 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.
  • The apps/flutter-test/ app should stay focused on package integration behavior.