260 lines
8.1 KiB
Markdown
260 lines
8.1 KiB
Markdown
# mattermost_push_plugin
|
|
|
|
Android-first Flutter plugin for Mattermost push notifications.
|
|
|
|
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 Mattermost signing-key storage.
|
|
|
|
## 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
|
|
|
|
```text
|
|
mattermost-push-plugin/
|
|
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
|
|
example/ # Thin Flutter app used as the integration test harness
|
|
test/ # Widget tests for the harness app
|
|
integration_test/ # Device/emulator integration tests
|
|
```
|
|
|
|
The `example/` app is the official independent consumer app for this repository.
|
|
Use it to validate plugin integration without coupling tests to any real product
|
|
app. Consuming apps should only test their own wiring, such as initialization,
|
|
credential handoff, and navigation callbacks.
|
|
|
|
## How It Works
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Mattermost 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 `MattermostPushPlugin.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 Mattermost server.
|
|
7. Connect notification-open callbacks to the host app router.
|
|
|
|
## Installation
|
|
|
|
For local development or another app in the same workspace, use a path
|
|
dependency:
|
|
|
|
```yaml
|
|
dependencies:
|
|
mattermost_push_plugin:
|
|
path: ../mattermost-push-plugin
|
|
```
|
|
|
|
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:mattermost_push_plugin/mattermost_push_plugin.dart';
|
|
```
|
|
|
|
Initialize the plugin:
|
|
|
|
```dart
|
|
await MattermostPushPlugin.instance.initialize();
|
|
```
|
|
|
|
Listen to raw notification events:
|
|
|
|
```dart
|
|
MattermostPushPlugin.instance.onNotification.listen((data) {
|
|
// data includes the native payload plus a push type.
|
|
});
|
|
```
|
|
|
|
Listen to notification-open events:
|
|
|
|
```dart
|
|
MattermostPushPlugin.instance.onNotificationOpened.listen((event) {
|
|
final serverUrl = event.serverUrl;
|
|
final channelId = event.channelId;
|
|
final rootId = event.rootId;
|
|
});
|
|
```
|
|
|
|
Register navigation callbacks:
|
|
|
|
```dart
|
|
MattermostPushPlugin.instance.onNavigateToChannel = (
|
|
String serverUrl,
|
|
String channelId,
|
|
) {
|
|
// Navigate to the channel in the host app.
|
|
};
|
|
|
|
MattermostPushPlugin.instance.onNavigateToThread = (
|
|
String serverUrl,
|
|
String rootId,
|
|
) {
|
|
// Navigate to the CRT thread in the host app.
|
|
};
|
|
```
|
|
|
|
Handle device-token registration:
|
|
|
|
```dart
|
|
MattermostPushPlugin.instance.onDeviceTokenReady = (String deviceToken) {
|
|
// Example format: android_rn-v2:YOUR_FCM_TOKEN
|
|
// Send this token to the Mattermost server.
|
|
};
|
|
```
|
|
|
|
Store credentials and signing keys:
|
|
|
|
```dart
|
|
await MattermostPushPlugin.instance.setAuthToken(
|
|
'https://mattermost.example.com',
|
|
'USER_AUTH_TOKEN',
|
|
identifier: 'user_session_id',
|
|
);
|
|
|
|
await MattermostPushPlugin.instance.setSigningKey(
|
|
'https://mattermost.example.com',
|
|
'SERVER_PUBLIC_SIGNING_KEY',
|
|
);
|
|
|
|
await MattermostPushPlugin.instance.clearAuthToken(
|
|
'https://mattermost.example.com',
|
|
);
|
|
|
|
final token = await MattermostPushPlugin.instance.getDeviceToken();
|
|
```
|
|
|
|
## Event Types
|
|
|
|
The Dart API currently recognizes these native event types:
|
|
|
|
| Type | Meaning |
|
|
| --- | --- |
|
|
| `message` | A Mattermost 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.
|
|
|
|
## Android Notes
|
|
|
|
The Android manifest declares the plugin's own
|
|
`MattermostFirebaseMessagingService` 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
|
|
`example/android/local.properties`.
|
|
|
|
## Development Commands
|
|
|
|
Run Dart analysis and unit tests from the repository root:
|
|
|
|
```sh
|
|
flutter analyze
|
|
flutter test
|
|
```
|
|
|
|
Run harness-app checks:
|
|
|
|
```sh
|
|
cd example
|
|
flutter analyze
|
|
flutter test
|
|
flutter test integration_test
|
|
```
|
|
|
|
Run Android native unit tests:
|
|
|
|
```sh
|
|
cd example/android
|
|
./gradlew testDebugUnitTest
|
|
```
|
|
|
|
## 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.
|
|
|
|
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. `example/test/` covers the thin Flutter harness app.
|
|
4. `example/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 Mattermost ACK
|
|
flows because they depend on external infrastructure.
|
|
|
|
## Manual Smoke Checklist
|
|
|
|
Use this checklist before promoting a plugin change to a consuming app:
|
|
|
|
- [ ] FCM payload wakes `MattermostFirebaseMessagingService`.
|
|
- [ ] Valid Mattermost 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 Mattermost 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 `example/` app is intentionally small and should not grow into a product
|
|
app.
|