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.
example/lib/main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:notify_mvp/notify_mvp.dart';
// ── Background message handler (must be top-level function) ──────────────────
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
debugPrint('[NotifyMVP Example] Background message: ${message.messageId}');
}
// ── Entry point ───────────────────────────────────────────────────────────────
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Initialize Firebase first
await Firebase.initializeApp(
// Replace with your generated firebase_options.dart
// options: DefaultFirebaseOptions.currentPlatform,
);
// 2. Register background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 3. Initialize NotifyMVP SDK
// Replace these values with your actual NotifyMVP credentials
final result = await NotifyMVP.initialize(
appId: 'app_your_app_id', // From NotifyMVP dashboard → Projects
apiKey: 'your_api_key_here', // From NotifyMVP dashboard → Projects
baseUrl: 'https://your-worker.workers.dev', // your Cloudflare Worker URL
debugLogging: true, // Set false in production
autoRegister: true, // Auto-registers on init
);
if (result.isSuccess) {
debugPrint('NotifyMVP: Device registered successfully!');
} else {
debugPrint('NotifyMVP: Registration failed — ${result.error}');
}
runApp(const MyApp());
}
// ── App ───────────────────────────────────────────────────────────────────────
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NotifyMVP Example',
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.dark,
),
),
home: const HomePage(),
);
}
}
// ── Home Page ─────────────────────────────────────────────────────────────────
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _status = 'Initializing…';
String _fcmToken = 'Not available';
final List<String> _receivedMessages = [];
@override
void initState() {
super.initState();
_setup();
}
Future<void> _setup() async {
// Update UI with SDK status
setState(() {
_status = NotifyMVP.isInitialized ? 'Registered ✓' : 'Not registered';
_fcmToken = NotifyMVP.fcmToken ?? 'Unavailable';
});
// Listen for foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
setState(() {
final title = message.notification?.title ?? '(no title)';
final body = message.notification?.body ?? '(no body)';
_receivedMessages.insert(0, '$title: $body');
});
// Show a snackbar when a foreground notification arrives
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${message.notification?.title ?? 'Notification'}: '
'${message.notification?.body ?? ''}',
),
behavior: SnackBarBehavior.floating,
),
);
}
});
// Handle notification tap when app is in background (not terminated)
NotifyMVP.onNotificationOpened().listen((payload) {
debugPrint('[NotifyMVP] Opened from background: ${payload.url}');
setState(() {
_receivedMessages.insert(
0,
payload.url != null
? '${payload.title}: ${payload.body} → ${payload.url}'
: '${payload.title}: ${payload.body}',
);
});
// Navigate with GoRouter / Navigator using payload.url
});
// Handle notification tap when app was terminated
final initial = await NotifyMVP.getInitialNotification();
if (initial != null) {
debugPrint('[NotifyMVP] Opened from terminated: ${initial.url}');
setState(() {
_receivedMessages.insert(
0,
initial.url != null
? '${initial.title}: ${initial.body} → ${initial.url}'
: '${initial.title}: ${initial.body}',
);
});
}
}
Future<void> _manualRegister() async {
setState(() => _status = 'Registering…');
final result = await NotifyMVP.register();
setState(() {
_status = result.isSuccess ? 'Registered ✓' : 'Failed: ${result.error}';
_fcmToken = NotifyMVP.fcmToken ?? 'Unavailable';
});
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('NotifyMVP SDK Demo'),
backgroundColor: colorScheme.surface,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status card
_InfoCard(
title: 'SDK Status',
value: _status,
color: _status.contains('✓')
? Colors.green.shade400
: _status.contains('Failed')
? Colors.red.shade400
: Colors.amber.shade400,
),
const SizedBox(height: 12),
// FCM token card
_InfoCard(
title: 'FCM Token',
value: _fcmToken.length > 40
? '${_fcmToken.substring(0, 20)}…${_fcmToken.substring(_fcmToken.length - 20)}'
: _fcmToken,
color: colorScheme.primary,
),
const SizedBox(height: 16),
// Manual register button
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _manualRegister,
icon: const Icon(Icons.refresh),
label: const Text('Re-register Device'),
),
),
const SizedBox(height: 24),
// Received messages
Text(
'Received Notifications',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Expanded(
child: _receivedMessages.isEmpty
? Center(
child: Text(
'No notifications received yet.\nSend one from the NotifyMVP dashboard.',
textAlign: TextAlign.center,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
)
: ListView.separated(
itemCount: _receivedMessages.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(
dense: true,
leading: const Icon(Icons.notifications_outlined, size: 18),
title: Text(
_receivedMessages[index],
style: const TextStyle(fontSize: 13),
),
),
),
),
],
),
),
);
}
}
// ── Reusable info card widget ─────────────────────────────────────────────────
class _InfoCard extends StatelessWidget {
final String title;
final String value;
final Color color;
const _InfoCard({
required this.title,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
letterSpacing: 0.8,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: color,
fontFamily: 'monospace',
),
),
],
),
);
}
}