671 lines
32 KiB
Markdown
671 lines
32 KiB
Markdown
# 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](#push-payload-fields)
|
|
and the [ACK Contract](#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`](../../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 | Baseline | Plugin registration (`NexoMessagingWeb.registerWith()`), `package:web` dependency, and browser Notification interop boundary (`BrowserNotificationInterop`) are in place. Public permission/request/display APIs are reserved for a future Epic. Full Web Push (service worker, VAPID, server device-token registration) follows. |
|
|
|
|
## Repository Layout
|
|
|
|
```text
|
|
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
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```dart
|
|
import 'package:nexo_messaging/nexo_messaging.dart';
|
|
```
|
|
|
|
Initialize the plugin:
|
|
|
|
```dart
|
|
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
|
|
NexoMessagingPlugin.instance.onNotification.listen((data) {
|
|
// data includes the native payload plus a push type.
|
|
});
|
|
```
|
|
|
|
Listen to notification-open events:
|
|
|
|
```dart
|
|
NexoMessagingPlugin.instance.onNotificationOpened.listen((event) {
|
|
final serverUrl = event.serverUrl;
|
|
final channelId = event.channelId;
|
|
final rootId = event.rootId;
|
|
});
|
|
```
|
|
|
|
Register navigation callbacks:
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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();
|
|
```
|
|
|
|
## 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`.
|
|
|
|
Remaining for the foreground Epic:
|
|
|
|
- Public permission / display / click routing APIs.
|
|
- Notification event forwarding through the main plugin stream.
|
|
|
|
## 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. |
|
|
|
|
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`](../../services/core/compose/README.md#sdk-contract-anchors).
|
|
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:
|
|
|
|
```sh
|
|
flutter analyze
|
|
flutter test
|
|
```
|
|
|
|
Run `flutter-test` checks from the app path:
|
|
|
|
```sh
|
|
cd apps/flutter-test
|
|
flutter analyze
|
|
flutter test
|
|
flutter test integration_test
|
|
```
|
|
|
|
Run Android native unit tests:
|
|
|
|
```sh
|
|
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`](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`](../../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`](../../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. However:
|
|
- Public permission / display / click routing APIs are not yet exposed.
|
|
- Notification events are not yet forwarded through the main plugin stream.
|
|
- 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.
|