notify_mvp 1.0.3
notify_mvp: ^1.0.3 copied to clipboard
Official Flutter SDK for NotifyMVP — a OneSignal-like push notification platform. Handles FCM token registration, auto-refresh, and device identification with zero boilerplate.
notify_mvp — Flutter SDK #
Official Flutter SDK for NotifyMVP — a self-hosted push notification platform.
What it does #
- Requests FCM (Firebase Cloud Messaging) permission
- Registers the device token with your NotifyMVP backend
- Auto re-registers when the FCM token refreshes
- Persists a stable
deviceIdacross app restarts using secure storage - Retries failed registrations with exponential backoff
Installation #
Add to your app's pubspec.yaml:
dependencies:
notify_mvp: ^1.0.3
firebase_core: ^3.0.0
firebase_messaging: ^15.0.0
Run:
flutter pub get
baseUrl #
baseUrl is your NotifyMVP Cloudflare Worker URL — the origin you get after deploying my-app/ with Wrangler.
https://notifymvp.<your-account>.workers.dev- or custom domain:
https://notify.yourdomain.com
No trailing slash. Not a shared public API.
Setup #
1. Configure Firebase #
Follow the FlutterFire setup guide to:
- Create a Firebase project
- Add your Android/iOS apps
- Download
google-services.json(Android) andGoogleService-Info.plist(iOS) - Run
flutterfire configureto generatefirebase_options.dart
2. Android — android/app/build.gradle (or build.gradle.kts) #
A. Apply Google Services Plugin:
plugins {
id 'com.google.gms.google-services'
}
B. Enable Core Library Desugaring (Required for High-Priority Notifications):
Why is this required?
NotifyMVP Flutter SDK delivers high-priority Heads-Up system notifications (pop-up banner with sound & vibration) using Java 8+ Android APIs. Enabling core library desugaring allows older Android versions to process these APIs without crashing.
If using Groovy (build.gradle):
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
If using Kotlin DSL (build.gradle.kts):
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
C. Add Default Notification Channel to android/app/src/main/AndroidManifest.xml:
Inside your <application> tag, add this <meta-data> entry so Android FCM assigns background notifications to the High-Priority Pop-up Channel (enabling top pop-up banners on mobile home screen):
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="notifymvp_heads_up_channel" />
3. iOS — ios/Runner/Info.plist #
Add background modes for push notifications:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
Usage #
Initialize in main.dart #
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:notify_mvp/notify_mvp.dart';
/// Background message handler — must be a top-level function
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
try {
await Firebase.initializeApp();
// Rich Push (Big Picture / buttons) when server sends notifymvp_rich=1
await notifyMvpFirebaseBackgroundHandler(message);
} catch (_) {}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Initialize Firebase & FCM Background Handler
try {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 2. Initialize NotifyMVP SDK
await NotifyMVP.initialize(
appId: 'app_your_app_id', // From dashboard → Projects
apiKey: 'your_api_key_here', // From dashboard → Projects
baseUrl: 'https://your-worker.workers.dev', // your Cloudflare Worker URL
debugLogging: false, // Set true in development
);
} catch (e) {
debugPrint('NotifyMVP / Firebase init info: $e');
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NotifyMVP App',
theme: ThemeData.dark(useMaterial3: true),
home: const HomeScreen(),
);
}
}
Rich Push (image, icon, action buttons) #
Send imageUrl / iconUrl from the NotifyMVP dashboard or REST API. The server sets notifymvp_rich=1 on data payloads so the SDK can render Big Picture reliably on Android.
Background handler (required for rich push when app is not in foreground):
import 'package:notify_mvp/notify_mvp.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
await notifyMvpFirebaseBackgroundHandler(message);
}
Foreground rich display is handled automatically after NotifyMVP.initialize().
Optional action buttons (Android) — include in FCM data:
"actions": "[{\"id\":\"open\",\"title\":\"Open\"},{\"id\":\"later\",\"title\":\"Later\"}]"
Or flat keys: action1_title, action1_id, action2_title, …
Image URLs must be public HTTPS (direct .jpg / .png links work best).
External User ID (OneSignal-style User Linking) #
Link a user ID (e.g., Firebase Auth UID, User Database ID) to target specific users in your notifications:
// Option A: Pass externalUserId on initialize
await NotifyMVP.initialize(
appId: 'app_a3f9bc12',
apiKey: 'your_api_key',
baseUrl: 'https://your-worker.workers.dev', // your Cloudflare Worker URL
externalUserId: 'user_12345', // Link user immediately
);
// Option B: Link user after login
await NotifyMVP.login('user_12345'); // or NotifyMVP.setExternalUserId('user_12345')
// Unlink user on logout
await NotifyMVP.logout();
Deep link & Notification Tap Handling (OneSignal-style) #
When a user taps a notification (from a foreground banner pop-up, background notification tray, or killed/terminated app state), the launch URL from the dashboard arrives in the payload.
Listening to Notification Taps (All States: Foreground, Background & Terminated)
@override
void initState() {
super.initState();
// 1. Check if app was launched by tapping a notification while terminated (Cold Start)
NotifyMVP.getInitialNotification().then((payload) {
if (payload?.url != null) {
debugPrint('Launched from terminated state with URL: ${payload!.url}');
_handleDeepLink(payload.url!);
}
});
// 2. Listen to notification taps while app is running (Foreground or Background tray tap)
NotifyMVP.onNotificationOpened().listen((payload) {
debugPrint('Notification clicked! Title: ${payload.title}, URL: ${payload.url}');
if (payload.url != null) {
_handleDeepLink(payload.url!);
}
});
}
void _handleDeepLink(String url) {
// Example: Navigate using GoRouter, Navigator, or GetX
// context.go(url);
// Get.toNamed(url);
}
GetX Integration Example
GetX allows context-less navigation anywhere in your app:
import 'package:get/get.dart';
import 'package:notify_mvp/notify_mvp.dart';
class NotificationService extends GetxService {
@override
void onInit() {
super.onInit();
_listenToNotifications();
}
void _listenToNotifications() {
// 1. Cold start launch (app was killed)
NotifyMVP.getInitialNotification().then((payload) {
if (payload?.url != null) _navigateWithGetX(payload!.url!);
});
// 2. Foreground / Background notification tap
NotifyMVP.onNotificationOpened().listen((payload) {
if (payload.url != null) _navigateWithGetX(payload.url!);
});
}
void _navigateWithGetX(String url) {
debugPrint('GetX Navigating to: $url');
if (url.startsWith('/')) {
Get.toNamed(url);
} else {
Get.toNamed('/$url');
}
}
}
Payload Data Fields
The NotifyOpenedPayload object contains:
payload.title— Notification Titlepayload.body— Notification Bodypayload.url— Launch URL / Deep Link string (data['url']ordata['link'])payload.data— Full custom data map sent from dashboard or API
Manual registration #
// Re-register (e.g. after permissions change)
final result = await NotifyMVP.register();
if (result.isSuccess) {
print('Registered! deviceId: ${result.data?['deviceId']}');
} else {
print('Failed: ${result.error}');
}
API Reference #
NotifyMVP.initialize(...) #
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
appId |
String |
✅ | — | Your NotifyMVP App ID |
apiKey |
String |
✅ | — | Your NotifyMVP API Key |
baseUrl |
String |
✅ | — | Your Cloudflare Worker URL (https://your-worker.workers.dev or custom domain) |
externalUserId |
String? |
❌ | null |
Optional User ID (Firebase UID / DB ID) |
debugLogging |
bool |
❌ | false |
Enable console logs |
autoRegister |
bool |
❌ | true |
Register on init |
Returns NotifyResult — check .isSuccess and .error.
NotifyMVP.login(userId) / setExternalUserId(userId) #
Link this device subscription to a user ID for targeted pushes.
NotifyMVP.logout() #
Unlink user ID from this device subscription.
NotifyMVP.register() #
Manually register or re-register the device. Returns NotifyResult.
NotifyMVP.fcmToken #
String? — the current FCM token. Null until initialized.
NotifyMVP.isInitialized #
bool — whether the SDK has been initialized.
NotifyConfig (advanced) #
Pass a custom config object for fine-grained control:
final config = NotifyConfig(
appId: 'app_a3f9bc12',
apiKey: 'your_api_key',
baseUrl: 'https://your-worker.workers.dev', // your Cloudflare Worker URL
debugLogging: true,
requestTimeout: const Duration(seconds: 15),
maxRetries: 3,
);
await NotifyMVP.initialize(
appId: config.appId,
apiKey: config.apiKey,
baseUrl: config.baseUrl,
config: config,
);
How it works #
Flutter App NotifyMVP Backend Firebase FCM
│ │ │
│── NotifyMVP.initialize() ───────►│ │
│ │ │
│◄── Request FCM permission ───────┤ │
│ │ │
│── getToken() ────────────────────┼────────────────────────►│
│◄── FCM token ────────────────────┼─────────────────────────│
│ │ │
│── POST /api/device/register ────►│ │
│ { appId, apiKey, fcmToken, │ │
│ deviceId, platform } │ │
│◄── { success: true } ────────────│ │
│ │ │
│ Token saved in DB │
│ │ │
│ [later — dashboard send] │ │
│ │── FCM multicast ───────►│
│◄── Push notification ────────────┼─────────────────────────│
Security notes #
- The
apiKeyis embedded in your app binary — this is expected and intentional (same model as OneSignal, Firebase, etc.) - The API key only allows device registration — it cannot read data, send notifications, or access other projects
- Sending notifications requires the dashboard (server-side only) — the apiKey alone cannot trigger sends
deviceIdis stored in encrypted secure storage (flutter_secure_storage)
Platform support #
| Platform | Status |
|---|---|
| Android | ✅ |
| iOS | ✅ |
| Flutter Web | ❌ (web push not in scope for MVP) |
| macOS | ⚠️ (untested) |
| Windows/Linux | ❌ (FCM not supported) |
Troubleshooting #
"FCM token unavailable — notification permission may be denied"
→ User denied notification permission. Show a rationale UI and call NotifyMVP.register() again after they grant permission.
"Invalid appId or apiKey" → Double-check your App ID and API Key in the NotifyMVP dashboard → Projects.
"Request failed after 3 attempts"
→ Check your baseUrl is correct and the server is reachable.
Token not refreshing
→ The SDK listens to FirebaseMessaging.instance.onTokenRefresh automatically. Make sure NotifyMVP.initialize() is called before runApp().