| .. | ||
| android | ||
| docs | ||
| ios | ||
| lib | ||
| macos | ||
| test | ||
| .gitignore | ||
| .metadata | ||
| analysis_options.yaml | ||
| CHANGELOG.md | ||
| LICENSE | ||
| pubspec.yaml | ||
| README.md | ||
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 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, behavior is not implemented yet |
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
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:
- Configure Firebase for the host app, including
google-services.jsonunderandroid/app/. - Initialize Firebase as required by the host app before push registration.
- Call
NexoMessagingPlugin.instance.initialize()during app startup. - Save auth tokens and signing keys after login.
- Clear auth tokens on logout.
- Send the formatted device token to the nexo server.
- Connect notification-open callbacks to the host app router.
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();
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) {
// Example format: android_rn-v2:YOUR_FCM_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();
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.
- The host app configures Firebase, initializes Firebase for its app, and calls
NexoMessagingPlugin.instance.initialize()during startup. initialize()subscribes to native events, listens for FCM token refreshes, requests notification permission, and reads the current FCM token.- Dart formats each raw FCM token as
android_rn-v2:<raw-token>before calling the nativesaveDeviceTokenaction.onDeviceTokenReadyreceives the formatted token, andgetDeviceToken()returns the last formatted token stored by native code. - 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.
- 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 containingserver_idcan be resolved to a server URL. - 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. - 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.
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. |
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 |
none | token is the raw FCM token from native or FlutterFire. Dart formats it as android_rn-v2:<raw-token> before native storage and onDeviceTokenReady. |
NotificationOpenedEvent parses server_url, channel_id, root_id, and
is_crt_enabled from opened-compatible events. The current parser treats only
the string value "true" 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 |
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 | The string value "true" 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_idand raw device token, and the signing key resolves from the server database or thesetSigningKey()fallback. - A payload without
server_urlis valid only whenserver_idor the one-server fallback resolves to a server URL before ACK, reply, or signature lookup. - Payload fields not listed here may pass through raw
onNotificationmaps for diagnostics, but consuming apps should not treat them as stable API.
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:
- If ACK delivery fails, Android logs the failure and continues signature validation and notification processing 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, that is a runtime/server state failure, not a Dart API contract change.
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.
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:
- Root
test/covers Dart API behavior, event parsing, callback routing, and device-token formatting. android/src/test/should cover native payload parsing, signing-key lookup, local storage, notification behavior, and ACK request behavior.apps/flutter-test/test/covers the thin Flutter test app.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.- 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_urlor resolvableserver_id,channel_id, and a valid signature.
Evidence to capture:
-
logcatlines fromNexoFirebaseMessagingServicefor 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, andonNavigateToThreadrouting. -
FCM payload wakes
NexoFirebaseMessagingService. -
Valid nexo push payload passes signing-key verification.
-
Invalid or unsigned payload is dropped.
-
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.
- Full FCM delivery cannot be reliably automated in a headless test runner.
- The
apps/flutter-test/app should stay focused on package integration behavior.