firebase_messaging_handler 0.1.1-beta.1
firebase_messaging_handler: ^0.1.1-beta.1 copied to clipboard
Flutter FCM superpack: unified click stream (FG/BG/terminated), auto-initial handling, data-only bridge, inbox, in-app UX, badges, sounds, and web polish.
Firebase Messaging Handler Plugin #
π― One-Stop Push & In-App Messaging for Firebase Cloud Messaging β Handle everything from reliable click streams to scheduling, actions, quiet hours, and rich in-app templates. Zero breaking changes, maximum flexibility!
π Table of Contents #
- π Quick Start
- β¨ Key Features
- π§° What You Get
- π¦ Installation
- π§ Setup
- π Usage Examples
- ποΈ Advanced Features
- πͺ In-App Messaging
- π‘οΈ Foreground Notification Customization
- π Analytics Integration
- π©Ί Notification Diagnostics
- π Quiet Hours & Throttling
- π Data-Only Bridging
- π§ͺ Testing Utilities
- π¦ Payload Cookbook
- π API Reference
- π§ Configuration
- π Troubleshooting
- π€ Contributing
- π License
- π Support
- π What's Next?
π Quick Start #
import 'package:firebase_messaging_handler/firebase_messaging_handler.dart';
// Initialize the plugin
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: true,
),
],
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
// Send token to your backend
print('FCM Token: $fcmToken');
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
print('Notification clicked: ${data.title}');
// Handle notification click
}
});
β¨ Key Features #
π― Core Features #
- π± Cross-Platform Support - Android, iOS, and Web
- π Unified Notification Stream - Handle all notification types in one place
- ποΈ Flexible Initial Notification Control - Stream or separate handling
- π Smart Token Management - Automatic optimization with single backend call
- π‘οΈ Robust Error Handling - Comprehensive error recovery and logging
π¨ Advanced Features #
- β‘ Interactive Notification Actions - Custom buttons with payload handling
- β° Notification Scheduling - One-time and recurring notifications
- π’ Badge Management - Cross-platform badge count management
- π¦ Notification Grouping - Android groups and iOS conversation threads
- π Custom Sound Support - Platform-specific sound customization
- π Built-in Analytics - Track all notification events automatically
- π§ͺ Testing Utilities - Mock data and streams for comprehensive testing
- π©Ί Notification Doctor - Diagnose permissions, tokens, badges, and background wiring in seconds
- π Web-Safe Fallbacks - Gracefully degrade scheduling/actions/badges when unsupported in browsers
- π Quiet Hours & Frequency Caps - Control delivery cadence with lifecycle-aware helpers
- π Data-Only Bridging - Promote silent payloads into local notifications when needed
- π₯ Inbox Storage - Typed inbox model with SharedPreferences default and in-memory test store for read/delete flows
- π Unified Handler - Single callback for foreground/background/terminated with normalized payloads
- πͺ In-App Messaging - Trigger rich in-app templates from silent FCM payloads
- π‘οΈ Foreground Controls - Fully customize fallback foreground notifications
- π In-App Templates - Welcome, promotion, alert, success, and info templates
- π Activity Timeline - Persistent notification history with detailed timestamps
- π Smart Retry Logic - Intelligent Firebase setup retry based on error type
- π οΈ Professional Setup - Guided Firebase configuration with package name guidance
π§° What You Get #
Your app starts simple and scales only when you opt in. Every capability ships with safe defaults and a straightforward toggle.
- Core (always on): unified click stream, terminated-notification getter, token lifecycle management, platform badge helpers.
- Optional power-ups: scheduling, recurring rules, grouping, custom sounds, analytics callbacks, in-app templates, foreground overrides, mock/testing utilities.
- Zero extra deps: the plugin bundles
firebase_messagingfor youβadd one dependency and you are ready for push, scheduling, analytics, and in-app flows. - Progressive adoption: wire up the click stream today, add interactive actions or in-app templates later without touching existing code.
- Configuration-at-callsite: all advanced APIs expose per-call parameters so you can tailor a single notification without changing global settings.
- Navigation flexibility: Showcase example routes via a root
Navigatorkey, demonstrating payload-driven navigation without relying on a BuildContext.
Beta channel #
- Install the beta:
firebase_messaging_handler: 0.1.1-beta.1 - Includes: auto initial-notification stream, unified handler, inbox widget + storage, data-only bridge, payload validator, refreshed docs, and new tests/goldens.
- Stable users can stay pinned to
0.1.0until ready to adopt the beta line.
ποΈ Architecture Benefits #
- π§ Modular Design - Clean separation of concerns
- π§ͺ Better Testability - Interface-based design enables easy mocking
- π Enhanced Scalability - Easy to extend and maintain
- π Backward Compatible - Existing code works unchanged
- β‘ Better Performance - Optimized service interactions
π¦ Installation #
Add this to your package's pubspec.yaml file:
dependencies:
firebase_messaging_handler: ^latest_version
π§ Setup #
β‘ Quick Setup (5 Minutes) #
-
Add dependency:
dependencies: firebase_messaging_handler: ^latest_version -
Add basic permissions to
android/app/src/main/AndroidManifest.xml:<!-- Basic Firebase Messaging --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />π‘ Need more features? See the Detailed Permissions Guide below.
-
Initialize in your app:
await FirebaseMessagingHandler.instance.init( senderId: 'your_sender_id', androidChannelList: [/* channels */], androidNotificationIconPath: '@drawable/ic_notification', ); -
(Optional) Wire the background handler:
await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler( firebaseMessagingHandlerBackgroundDispatcher, );Use your own top-level handler if you need custom logicβjust remember to call
FirebaseMessagingHandler.handleBackgroundMessage(message)first.
Unified Handler (all lifecycles) #
await FirebaseMessagingHandler.instance.setUnifiedMessageHandler(
(NormalizedMessage message, NotificationLifecycle lifecycle) async {
debugPrint('[unified] lifecycle=$lifecycle title=${message.title}');
// Return true to mark handled and skip default rendering; false to let the plugin render/queue.
if (lifecycle == NotificationLifecycle.foreground) {
// e.g., custom in-app banner instead of system notification
return true;
}
return false;
},
);
Handler receives normalized fields (id, title, body, data, channelId, analytics, lifecycle, rawMessage). Works for foreground, background, resume, and terminated paths.
- Done! Your app now handles Firebase notifications.
π― Minimal Setup (Basic Notifications Only) #
For apps that only need basic push notifications:
<!-- Minimal permissions for basic notifications -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
What you get:
- β Push notifications from Firebase
- β Background message handling
- β Notification vibration
- β No scheduled notifications
- β No foreground notifications
- β No advanced features
π Detailed Setup #
1. Firebase Project Setup #
- Create a Firebase project at Firebase Console
- Add your Android and iOS apps to the project
- Download configuration files:
google-services.jsonβandroid/app/GoogleService-Info.plistβios/Runner/
2. Platform Configuration #
Android Setup
Add to android/app/build.gradle:
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
π Android Permissions Guide
Choose only the permissions you need based on your features:
π± Basic Notifications (Most Apps Need This) #
<!-- REQUIRED: Basic Firebase Messaging -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- REQUIRED: Notification Display -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
When to use: Basic push notifications, message handling, foreground notifications
β° Scheduled Notifications (Optional) #
<!-- REQUIRED: Scheduled Notifications -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
When to use: Only if you use scheduleNotification() or scheduleRecurringNotification()
π Advanced Features (Optional) #
<!-- REQUIRED: Background Processing -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- REQUIRED: Notification Actions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
When to use: Interactive notifications, background processing, notification actions
π Quick Decision Guide:
| Feature | Permissions Needed |
|---|---|
| Basic push notifications | INTERNET + WAKE_LOCK + VIBRATE + FOREGROUND_SERVICE |
| Scheduled notifications | Add SCHEDULE_EXACT_ALARM + USE_EXACT_ALARM + RECEIVE_BOOT_COMPLETED |
| Interactive notifications | Add FOREGROUND_SERVICE (already included in basic) |
| Background processing | Add RECEIVE_BOOT_COMPLETED |
π‘ Pro Tip: Start with basic permissions, then add more as you implement features!
β Why These Permissions?
| Permission | Why We Need It | What Happens Without It |
|---|---|---|
INTERNET |
Firebase messaging requires internet connection | β No push notifications |
WAKE_LOCK |
Keeps device awake to process background messages | β Messages lost when device sleeps |
VIBRATE |
Makes notifications noticeable | β Silent notifications only |
FOREGROUND_SERVICE |
Shows notifications when app is active | β No foreground notifications |
SCHEDULE_EXACT_ALARM |
Allows precise notification timing | β Scheduled notifications fail |
USE_EXACT_ALARM |
Required for exact alarm scheduling | β Scheduled notifications fail |
RECEIVE_BOOT_COMPLETED |
Restores scheduled notifications after reboot | β Scheduled notifications lost after reboot |
iOS Setup
Add to ios/Runner/AppDelegate.swift:
import Firebase
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
π iOS APNs Setup Required:
For iOS notifications to work properly, you MUST configure APNs:
-
Generate APNs Key:
- Go to Apple Developer Console
- Navigate to Certificates, Identifiers & Profiles
- Go to Keys section
- Create a new key with "Apple Push Notifications service (APNs)" enabled
- Download the
.p8key file
-
Upload to Firebase:
- Go to Firebase Console > Project Settings > Cloud Messaging
- Scroll to "Apple app configuration"
- Upload your APNs key (
.p8file) - Enter your Key ID and Team ID
- Choose environment: Sandbox (development) or Production
-
Without APNs setup:
- iOS notifications will NOT work
- FCM tokens will show "APNs token not set" error
- This is normal behavior until APNs is configured
β οΈ This is a Firebase requirement, not our plugin limitation!
Web Setup (Optional)
Add to web/index.html:
<script src="https://www.gstatic.cn/firebasejs/9.0.0/firebase-app.js"></script>
<script src="https://www.gstatic.cn/firebasejs/9.0.0/firebase-messaging.js"></script>
Browser caveats: Browsers do not support local scheduling, notification action buttons, or app-icon badges. Calls to those APIs are safely ignored and surfaced by the diagnostics helper.
π Usage Examples #
π― Complete Working Example #
import 'package:flutter/material.dart';
import 'package:firebase_messaging_handler/firebase_messaging_handler.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Initialize Firebase Messaging Handler
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
),
],
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
print('FCM Token: $fcmToken');
// Send token to your backend
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
print('Notification clicked: ${data.title}');
// Handle notification click
}
});
// Initial launch notifications are emitted onto the same stream by default.
// Set includeInitialNotificationInStream: false to opt out if you need to
// defer handling (e.g., until after auth).
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Messaging Handler Demo',
home: MyHomePage(),
);
}
}
### **π Showcase Example App**
The `example/` directory doubles as an FCM showcase powered entirely by this plugin:
- **Guided onboarding banner** β copy your FCM token, open the Firebase console, and follow the three-step testing loop.
- **Quick start scenarios** β fire interactive pushes, schedule one-time or recurring notifications, and generate Android groups with a tap.
- **Power utilities** β update badges, register custom sound channels, and clear demo data while analytics events stream in.
- **Scenario detail screen** β every notification routes to a dedicated inspector showing payloads, actions, badges, and metadata.
- **Activity timeline** β watch a running log of everything the handler does (initialization, scheduling, clears, custom actions).
- **Template playground** β paste sample silent payloads to preview the generic template renderer in real time.
Run `flutter run` inside `example/` to explore the full experience.
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FCM Handler Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
// Show a test notification
await FirebaseMessagingHandler.instance.showNotificationWithActions(
title: 'Test Notification',
body: 'This is a test notification',
actions: [
NotificationAction(
id: 'reply',
title: 'Reply',
payload: {'action': 'reply'},
),
NotificationAction(
id: 'dismiss',
title: 'Dismiss',
payload: {'action': 'dismiss'},
),
],
);
},
child: Text('Send Test Notification'),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
// Schedule a notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'Scheduled Notification',
body: 'This notification was scheduled',
scheduledDate: DateTime.now().add(Duration(minutes: 1)),
);
},
child: Text('Schedule Notification'),
),
],
),
),
);
}
}
Basic Setup #
// Initialize the plugin
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: channels,
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
// Send token to your backend
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
// Handle notification click
}
});
Interactive Notifications #
// Show notification with action buttons
await FirebaseMessagingHandler.instance.showNotificationWithActions(
title: 'New Message',
body: 'You have a new message from John',
actions: [
NotificationAction(
id: 'reply',
title: 'Reply',
payload: {'action': 'reply', 'user_id': '123'},
),
NotificationAction(
id: 'view',
title: 'View',
payload: {'action': 'view', 'message_id': '456'},
),
NotificationAction(
id: 'dismiss',
title: 'Dismiss',
payload: {'action': 'dismiss'},
),
],
);
π₯ Inbox Storage (typed, persistent) #
Use the default SharedPreferences-backed inbox store to fuel a history or inbox UI with read/delete support.
final inbox = InboxStorageService();
await inbox.upsert(
NotificationInboxItem(
id: 'welcome',
title: 'Welcome!',
body: 'Thanks for installing the app.',
timestamp: DateTime.now(),
data: {'origin': 'campaign_welcome'},
),
);
final List<NotificationInboxItem> page =
await inbox.fetch(page: 0, pageSize: 20);
await inbox.markRead([page.first.id]);
await inbox.delete([page.first.id]);
For tests or ephemeral state, use
InMemoryInboxStorage, which keeps items purely in memory.
Inbox UI widget
NotificationInboxView(
storage: InboxStorageService(),
onItemTap: (item) {
// Navigate or open detail
},
onActionTap: (actionId, item) {
// Handle custom buttons stored in item.actions
},
onDelete: (ids) async {
// Optional: sync deletions to backend
},
);
Notification Scheduling #
// Schedule a one-time notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'Meeting Reminder',
body: 'Team meeting in 30 minutes',
scheduledDate: DateTime.now().add(Duration(minutes: 30)),
);
// Schedule a recurring notification
await FirebaseMessagingHandler.instance.scheduleRecurringNotification(
id: 2,
title: 'Daily Reminder',
body: 'Don\'t forget to check your tasks',
repeatInterval: 'daily',
hour: 9,
minute: 0,
);
Badge Management #
// Set badge count
await FirebaseMessagingHandler.instance.setIOSBadgeCount(5);
await FirebaseMessagingHandler.instance.setAndroidBadgeCount(3);
// Get badge count
final int iosBadge = await FirebaseMessagingHandler.instance.getIOSBadgeCount();
final int androidBadge = await FirebaseMessagingHandler.instance.getAndroidBadgeCount();
// Clear badge count
await FirebaseMessagingHandler.instance.clearBadgeCount();
Notification Grouping #
// Show grouped notifications
await FirebaseMessagingHandler.instance.showGroupedNotification(
title: 'New Messages',
body: 'You have 3 new messages',
groupKey: 'messages',
groupTitle: 'Messages',
isSummary: true,
);
// Create notification group
await FirebaseMessagingHandler.instance.createNotificationGroup(
groupKey: 'messages',
groupTitle: 'Messages',
notifications: [
NotificationData(
title: 'Message 1',
body: 'Hello from John',
payload: {'message_id': '1'},
),
NotificationData(
title: 'Message 2',
body: 'Hello from Jane',
payload: {'message_id': '2'},
),
],
);
Custom Sounds #
// Create custom sound channel
await FirebaseMessagingHandler.instance.createCustomSoundChannel(
channelId: 'custom_sound',
channelName: 'Custom Sound Notifications',
channelDescription: 'Notifications with custom sounds',
soundFileName: 'custom_sound.mp3',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
);
// Show notification with custom sound
await FirebaseMessagingHandler.instance.showNotificationWithCustomSound(
title: 'Custom Sound Notification',
body: 'This notification has a custom sound',
soundFileName: 'custom_sound.mp3',
);
Analytics Integration #
// Set up analytics callback
FirebaseMessagingHandler.instance.setAnalyticsCallback((event, data) {
print('Analytics Event: $event');
print('Event Data: $data');
// Send to your analytics service
// FirebaseAnalytics.instance.logEvent(name: event, parameters: data);
});
// Track custom events
FirebaseMessagingHandler.instance.trackAnalyticsEvent('custom_event', {
'user_id': '123',
'action': 'notification_clicked',
});
ποΈ Advanced Features #
Notification Actions #
Create interactive notifications with custom action buttons:
NotificationAction(
id: 'reply',
title: 'Reply',
destructive: false,
payload: {
'action': 'reply',
'user_id': '123',
'thread_id': '456',
},
)
Scheduling Options #
Schedule notifications with various options:
// One-time notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'One-time Notification',
body: 'This will show once',
scheduledDate: DateTime.now().add(Duration(hours: 1)),
allowWhileIdle: true,
);
// Recurring notification
await FirebaseMessagingHandler.instance.scheduleRecurringNotification(
id: 2,
title: 'Daily Reminder',
body: 'Daily task reminder',
repeatInterval: 'daily',
hour: 9,
minute: 0,
);
Badge Management #
Cross-platform badge count management:
// iOS badge management
await FirebaseMessagingHandler.instance.setIOSBadgeCount(5);
final int iosBadge = await FirebaseMessagingHandler.instance.getIOSBadgeCount();
// Android badge management
await FirebaseMessagingHandler.instance.setAndroidBadgeCount(3);
final int androidBadge = await FirebaseMessagingHandler.instance.getAndroidBadgeCount();
// Clear all badges
await FirebaseMessagingHandler.instance.clearBadgeCount();
Notification Grouping #
Organize notifications into groups:
// Android notification groups
await FirebaseMessagingHandler.instance.showGroupedNotification(
title: 'Group Summary',
body: '3 new notifications',
groupKey: 'messages',
groupTitle: 'Messages',
isSummary: true,
);
// iOS conversation threads
await FirebaseMessagingHandler.instance.showThreadedNotification(
title: 'New Message',
body: 'Hello from John',
threadIdentifier: 'conversation_123',
);
Custom Sound Support #
Platform-specific sound customization:
// Create custom sound channel
await FirebaseMessagingHandler.instance.createCustomSoundChannel(
channelId: 'custom_sound',
channelName: 'Custom Sound Notifications',
channelDescription: 'Notifications with custom sounds',
soundFileName: 'custom_sound.mp3',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
enableVibration: true,
enableLights: true,
);
// Get available sounds (iOS)
final List<String>? sounds = await FirebaseMessagingHandler.instance.getAvailableSounds();
πͺ In-App Messaging #
Deliver rich in-app experiences using silent/data-only FCM payloads that map to reusable templates.
Register Templates #
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'promo_banner': InAppNotificationTemplate(
id: 'promo_banner',
description: 'Lightweight promotional banner',
onDisplay: (inAppData) {
inAppOverlayController.showBanner(
title: inAppData.content['title'] as String?,
body: inAppData.content['body'] as String?,
imageUrl: inAppData.content['image'] as String?,
ctaLabel: inAppData.content['cta_label'] as String?,
deeplink: inAppData.content['deeplink'] as String?,
);
},
),
});
FirebaseMessagingHandler.instance.setInAppFallbackDisplayHandler((payload) {
debugPrint('Unhandled in-app template: ${payload.templateId}');
});
Listen for Ready Messages #
FirebaseMessagingHandler.instance
.getInAppNotificationStream()
.listen((inAppData) {
inAppLogger.debug('Render template ${inAppData.templateId}');
campaignAnalytics.track('in_app_impression', inAppData.analytics);
});
Need to hydrate pending payloads after a cold start? Call:
await FirebaseMessagingHandler.instance.flushPendingInAppNotifications();
Sample FCM Payload #
{
"message": {
"token": "{{deviceToken}}",
"data": {
"fcmh_inapp": "{ \"id\": \"winter_sale_2025\", \"templateId\": \"promo_banner\", \"trigger\": \"immediate\", \"content\": { \"title\": \"Winter Sale\", \"body\": \"Take 25% off today only\", \"cta_label\": \"Shop now\", \"deeplink\": \"app://store/sale\" }, \"analytics\": { \"campaignId\": \"winter_flash\", \"variant\": \"A\" } }"
}
}
}
Supported triggers:
immediateβ render as soon as the payload arrives (foreground or via queued stream)next_foregroundβ store until the next time you listen to the streamapp_launchβ store untilflushPendingInAppNotificationsis calledcustomβ surface the payload immediately and let the host decide when to display
Use clearPendingInAppNotifications() to drop queued payloads (optionally targeting a specific id).
Built-in Templates & Overlay Support #
π― Perfect Use Cases for In-App Templates:
- Feature announcements - Introduce new capabilities
- User onboarding - Guide users through app features
- Feedback collection - Gather user ratings and suggestions
- Promotional content - Showcase offers and campaigns
- Educational content - Tips, tutorials, and help
- User engagement - Surveys, polls, and interactive content
- Quick notifications - Snackbars for non-intrusive messages
β Avoid for Critical Updates:
- App updates - Use system-level update prompts instead
- Security alerts - Use push notifications for immediate attention
- Payment confirmations - Use dedicated UI flows
- Emergency notifications - Use push notifications for reliability
π Template Flexibility: These built-in templates are just examples! The plugin provides a flexible foundation where you can:
- Register custom templates with your own layouts and animations
- Create any UI component - modals, sheets, cards, overlays, etc.
- Define custom interactions - gestures, animations, transitions
- Build brand-specific experiences - match your app's design system
- Implement complex workflows - multi-step processes, wizards, etc.
The plugin handles the infrastructure (overlay management, navigation, analytics) while you build the experience!
Provide a navigator key so the handler can present rich layouts:
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
void main() {
WidgetsFlutterBinding.ensureInitialized();
FirebaseMessagingHandler.instance.setInAppNavigatorKey(rootNavigatorKey);
runApp(MaterialApp(
navigatorKey: rootNavigatorKey,
home: const ShowcaseHome(),
));
}
Register the generic template and handle button callbacks:
void _registerTemplates() {
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'builtin_generic': BuiltInInAppTemplates.generic(
onAction: (actionId, data) {
debugPrint('Template action: $actionId payload=${data.payload}');
},
),
});
}
Trigger locally (useful for testing) or remotely via a silent FCM payload:
InAppMessageManager.instance.triggerInAppNotification(
InAppNotificationData(
id: 'demo_${DateTime.now().millisecondsSinceEpoch}',
templateId: 'builtin_generic',
triggerType: InAppTriggerTypeEnum.immediate,
content: {
'layout': 'dialog',
'title': 'New Feature Available',
'subtitle': 'Enhanced notification controls',
'body': 'We\'ve added smart scheduling and quiet hours. Try them out!',
'imageUrl': 'https://via.placeholder.com/600x320/059669/ffffff?text=New+Feature',
'blurSigma': 16,
'cornerRadius': 20,
'buttons': [
{'id': 'try_now', 'label': 'Try Now', 'style': 'filled'},
{'id': 'learn_more', 'label': 'Learn More', 'style': 'outlined'},
{'id': 'dismiss', 'label': 'Not now', 'style': 'text', 'dismissOnly': true}
],
},
analytics: {'source': 'docs_demo'},
rawPayload: const {},
receivedAt: DateTime.now(),
),
);
Supported layouts include dialog, full_screen, bottom_sheet, banner, tooltip, carousel, and snackbar. Configure blur, barrier color, size factors, button styles, and HTML content directly from the payload.
Key payload fields:
layout: dialog | full_screen | bottom_sheet | banner | snackbarwidthFactor/heightFactor: fractions of the screen size (dialog + full screen)blurSigma&barrierColor: backdrop styling for dialogs/full screensbackgroundColor/textColor: hex (#RRGGBBor#AARRGGBB) or RGB mapshtml: optional HTML body rendered withflutter_widget_from_html_corebuttons: array of{ id, label, style (filled|outlined|text|link), dismissOnly }autoDismissSeconds: auto-dismiss duration for banners/snackbarsposition:toporbottomfor banner layoutpages: list of page maps (carousel) each supportingtitle,body,html,imageUrl, andbuttons
Custom Template Registration #
Create your own templates with complete control over UI and behavior:
// Register a custom template
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'my_custom_template': InAppNotificationTemplate(
id: 'my_custom_template',
description: 'Custom onboarding flow',
autoDismissDuration: null, // Manual dismiss
onDisplay: (data) {
// Your custom UI logic here
showDialog(
context: context,
builder: (context) => MyCustomOnboardingDialog(
title: data.content['title'],
steps: data.content['steps'],
onComplete: () => data.onAction?.call('completed', data),
),
);
},
),
'my_animated_banner': InAppNotificationTemplate(
id: 'my_animated_banner',
description: 'Animated promotional banner',
autoDismissDuration: const Duration(seconds: 5),
onDisplay: (data) {
// Custom animated banner with your branding
showAnimatedBanner(
message: data.content['message'],
backgroundColor: data.content['color'],
animation: SlideAnimation.fromTop(),
);
},
),
});
Custom Template Benefits:
- Complete UI control - Use any Flutter widget
- Brand consistency - Match your app's design system
- Advanced animations - Custom transitions and effects
- Complex interactions - Multi-step flows, gestures, etc.
- Platform-specific behavior - Different UIs per platform
- Integration flexibility - Connect to your existing components
π‘οΈ Foreground Notification Customization #
Own the fallback notification UI that appears while your app is active. The plugin includes smart fallback logic to ensure notifications always display, even when no channel ID is provided.
Smart Channel Fallback #
The plugin automatically handles Android notification channels with intelligent fallback:
- Channel Specified: If a notification includes a channel ID, that specific channel is used
- Channel Not Found: If the specified channel doesn't exist, falls back to the first available channel
- No Channel Provided: If no channel ID is specified, uses the first available channel
- No Channels Available: Logs an error and skips the notification (prevents crashes)
This ensures your Android foreground notifications always display, regardless of Firebase Console configuration.
Override Once, Anywhere #
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
FirebaseMessagingHandler.instance.setForegroundNotificationOptions(
ForegroundNotificationOptions(
androidBuilder: (context) {
final imageAsset = context.data['image_asset'] as String?;
if (imageAsset != null) {
return AndroidNotificationDetails(
'promo_channel',
'Promotions',
channelDescription: 'Foreground promos',
importance: Importance.max,
priority: Priority.high,
styleInformation: BigPictureStyleInformation(
DrawableResourceAndroidBitmap(imageAsset),
largeIcon: DrawableResourceAndroidBitmap(imageAsset),
),
);
}
return const AndroidNotificationDetails(
'default_channel',
'Default Notifications',
importance: Importance.max,
priority: Priority.high,
);
},
iosBuilder: (context) {
final imageName = context.data['image_asset'] as String?;
if (imageName == null) {
return const DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
);
}
return DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
attachments: [
DarwinNotificationAttachment('resource:///$imageName'),
],
);
},
),
);
The builders receive the real RemoteMessage, so you can map any data payload to advanced styles, media, icons, or badges. Return null to fall back to the plugin defaults, or set enabled: false to suppress the automatic notification entirely when you prefer a custom in-app surface.
Prefer static overrides? Use androidDefaults / iosDefaults to plug in prebuilt AndroidNotificationDetails or DarwinNotificationDetails instances without writing builders.
π Configure Default Custom Sounds #
Set custom sounds onceβthey'll apply to all foreground notifications automatically:
// Step 1: Configure default sounds for foreground notifications
FirebaseMessagingHandler.instance.setForegroundNotificationOptions(
ForegroundNotificationOptions(
// Android: Place sound file in android/app/src/main/res/raw/custom_sound.mp3
androidSoundFileName: 'custom_sound', // Without extension
// iOS: Place sound file in project (Runner/Sounds/custom_sound.aiff)
iosSoundFileName: 'custom_sound.aiff', // With extension
androidDefaults: const AndroidNotificationDetails(
'default_channel',
'Default Notifications',
importance: Importance.max,
priority: Priority.high,
),
iosDefaults: const DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
),
),
);
Platform-Specific Sound Configuration:
Android (Two Options):
Option 1: Default sound for all foreground notifications (shown above)
ForegroundNotificationOptions(
androidSoundFileName: 'custom_sound', // Applied to all foreground notifications
)
Option 2: Per-channel sounds (configured during init)
await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
soundFileName: 'default_sound', // Sound for this channel only
),
NotificationChannelData(
id: 'urgent_channel',
name: 'Urgent Alerts',
soundFileName: 'urgent_sound', // Different sound for urgent channel
),
],
androidNotificationIconPath: '@drawable/ic_notification',
);
iOS:
iOS doesn't have channels, so configure the default sound through ForegroundNotificationOptions:
ForegroundNotificationOptions(
iosSoundFileName: 'custom_sound.aiff', // Applied to ALL iOS notifications
)
β οΈ Important iOS Limitation:
Foreground Notifications (App Active):
- β
Custom sounds work - Via
ForegroundNotificationOptions.iosSoundFileName - β Full control - Our plugin handles these notifications
Background Notifications (App Killed/Backgrounded):
- β Custom sounds DON'T work - iOS system handles these directly
- β No control - System uses default notification sound
π‘ Workaround for Background Sounds:
For background notifications, configure the sound in your Firebase Console payload:
{
"notification": {
"title": "Background Notification",
"body": "This will use system default sound",
"sound": "custom_sound.aiff" // iOS will use this if file exists in app bundle
},
"apns": {
"payload": {
"aps": {
"sound": "custom_sound.aiff" // Alternative APNs-specific sound
}
}
}
}
Requirements for Background Sounds:
- Sound file must be in your iOS app bundle (added via Xcode)
- Sound file must be β€ 30 seconds
- Supported formats: AIFF, CAF, WAV
- If file doesn't exist, iOS falls back to default sound
Sound File Setup:
Android:
- Place sound file in
android/app/src/main/res/raw/ - Use filename without extension (e.g.,
custom_soundforcustom_sound.mp3) - Supported formats: MP3, OGG
iOS:
- Add sound file to Xcode project (via Xcode > Add Files)
- Ensure it's added to the target (check "Copy items if needed")
- Use filename with extension (e.g.,
custom_sound.aiff) - Supported formats: AIFF, CAF, WAV (up to 30 seconds)
π‘ Pro Tip:
- Android: Use
NotificationChannelData.soundFileNamefor per-channel sounds, orForegroundNotificationOptions.androidSoundFileNamefor all foreground notifications- iOS: Use
ForegroundNotificationOptions.iosSoundFileName- this is the ONLY way to set default sounds on iOS since iOS doesn't have channels
βΉοΈ Use
DrawableResourceAndroidBitmap,ByteArrayAndroidBitmap, orFilePathAndroidBitmapdepending on where your assets live. For iOS,DarwinNotificationAttachmentexpects a local resource URIβdownload remote media before attaching it.
π Analytics Integration #
Built-in Event Tracking #
The plugin automatically tracks these events:
notification_received- When notifications arrivenotification_clicked- When notifications are tappednotification_action- When action buttons are pressednotification_scheduled- When notifications are scheduledfcm_token- Token events (fetched, updated, error)
Custom Analytics #
// Set up analytics callback
FirebaseMessagingHandler.instance.setAnalyticsCallback((event, data) {
// Send to your analytics service
FirebaseAnalytics.instance.logEvent(
name: event,
parameters: data,
);
});
// Track custom events
FirebaseMessagingHandler.instance.trackAnalyticsEvent('custom_event', {
'user_id': '123',
'action': 'notification_clicked',
'timestamp': DateTime.now().toIso8601String(),
});
π©Ί Notification Diagnostics #
Stay ahead of production issues with a built-in "notification doctor". It inspects permissions, token state, badge capabilities, web support, and background wiring in one call.
Run the Doctor #
final diagnostics = await FirebaseMessagingHandler.instance.runDiagnostics();
debugPrint('Notification diagnostics: ${diagnostics.toMap()}');
if (!diagnostics.success || diagnostics.recommendations.isNotEmpty) {
for (final recommendation in diagnostics.recommendations) {
debugPrint('Recommendation β $recommendation');
}
}
What you get:
permissionsGrantedandauthorizationStatusβ current notification permission state.fcmTokenAvailableβ whether a token is cached viaupdateTokenCallback.badgeSupportedβ launcher/platform badge capability (best-effort on Android).webNotificationsAllowed/metadata['webPermission']β browser permission string.metadata['backgroundHandlerRegistered']β confirmsconfigureBackgroundMessageHandlerhas been invoked.pendingNotificationCountβ number of locally scheduled notifications.metadata['invalidPayloadCount']β how many malformed data-only payloads were rejected by the bridge/schema guard.
Background Message Helper #
Register a top-level handler once and reuse the pluginβs pipeline inside the isolate:
@pragma('vm:entry-point')
Future<void> myBackgroundHandler(RemoteMessage message) async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseMessagingHandler.handleBackgroundMessage(message);
// Custom logic: update analytics, hydrate local cache, etc.
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler(
myBackgroundHandler,
);
runApp(const MyApp());
}
Tip: Prefer the built-in
firebaseMessagingHandlerBackgroundDispatcherif you simply want to hydrate the plugin without extra logic:await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler( firebaseMessagingHandlerBackgroundDispatcher, );
Web Safeguards #
Scheduling, interactive actions, and app-icon badges are not available in browsers. The doctor highlights these limitations and the runtime API logs βignoredβ warnings so you can branch logic per platform.
π Quiet Hours & Throttling #
Control when in-app messages surface and how frequently campaigns fire.
await FirebaseMessagingHandler.instance.setInAppDeliveryPolicy(
const InAppDeliveryPolicy(
globalInterval: Duration(seconds: 30),
perTemplateInterval: Duration(minutes: 2),
perTemplateDailyCap: 5,
quietHours: InAppQuietHours(startHour: 22, endHour: 7),
),
);
globalIntervalenforces a cool-down between any two in-app presentations.perTemplateIntervalkeeps the same template from spamming the timeline.perTemplateDailyCaplimits impressions per template per day.quietHoursdefers delivery until the configured window closes. Deferred payloads are re-queued automatically with the diagnostics report showing their status.
π Data-Only Bridging #
Promote silent payloads into local notifications (or custom flows) so users still see timely updates.
// Promote data-only FCM payloads to local notifications automatically
FirebaseMessagingHandler.instance.enableDefaultDataOnlyBridge(
channelId: 'actions_channel',
titleKey: 'title',
bodyKey: 'body',
);
// Or wire your own handler and decide when work is complete
await FirebaseMessagingHandler.instance.configureBackgroundProcessingCallback(
(RemoteMessage message) async {
if (message.data['should_defer'] == 'true') {
return false; // enqueue for retry when app wakes up
}
// Custom processingβ¦
return true;
},
);
Use FirebaseMessagingHandler.handleBackgroundMessage(message) inside your top-level background function to hydrate local queues before running custom logic.
π§ͺ Testing Utilities #
Mock Data Generation #
// Create mock notification data
final NotificationData mockData = FirebaseMessagingHandler.createMockNotificationData(
title: 'Mock Notification',
body: 'This is a mock notification',
payload: {'test': 'data'},
type: NotificationTypeEnum.foreground,
);
// Create mock remote message
final RemoteMessage mockMessage = FirebaseMessagingHandler.createMockRemoteMessage(
title: 'Mock Message',
body: 'This is a mock message',
data: {'test': 'data'},
);
Test Mode #
// Enable test mode
FirebaseMessagingHandler.setTestMode(true);
// Get mock streams
final Stream<RemoteMessage>? mockNotificationStream =
FirebaseMessagingHandler.getMockNotificationStream();
final Stream<NotificationData>? mockClickStream =
FirebaseMessagingHandler.getMockClickStream();
// Add mock events
FirebaseMessagingHandler.addMockNotification(mockMessage);
FirebaseMessagingHandler.addMockClickEvent(mockData);
// Reset mock data
FirebaseMessagingHandler.resetMockData();
π¦ Payload Cookbook #
Jump-start backend integration with ready-to-send payloads:
Interactive Notification (Actions + Analytics) #
{
"message": {
"token": "<device-token>",
"notification": {
"title": "New Support Ticket",
"body": "Tap Reply to follow up without opening the app."
},
"data": {
"is_action": true,
"action_id": "reply",
"action_payload": {"ticket_id": "12345"},
"analytics": {"campaign": "support_reengage"}
}
}
}
Data-Only β Local Notification Bridge #
{
"message": {
"token": "<device-token>",
"data": {
"title": "Inventory Update",
"body": "SKU #48319 is back in stock!",
"deep_link": "app://inventory/48319"
}
}
}
In-App Template Trigger #
{
"message": {
"token": "<device-token>",
"data": {
"fcmh_inapp": {
"id": "promo-2025",
"templateId": "builtin_generic",
"trigger": "immediate",
"content": {
"layout": "html_modal",
"title": "Spring Launch",
"html": "<h2>Fresh features</h2><p>Try quiet hours + notification doctor today.</p>",
"buttons": [{"id": "explore", "label": "Explore", "style": "filled"}]
}
}
}
}
}
π API Reference #
Core Methods #
Initialization
Future<Stream<NotificationData?>?> init({
required String senderId,
required List<NotificationChannelData> androidChannelList,
required String androidNotificationIconPath,
Future<bool> Function(String fcmToken)? updateTokenCallback,
bool includeInitialNotificationInStream = true,
})
Initial Notification Handling
Future<NotificationData?> checkInitial() // optional fallback; auto-handled by default
Notification Display
Future<void> showNotificationWithActions({
required String title,
required String body,
required List<NotificationAction> actions,
Map<String, dynamic>? payload,
String? channelId,
int? notificationId,
})
Future<void> showNotificationWithCustomSound({
required String title,
required String body,
required String soundFileName,
String? channelId,
Map<String, dynamic>? payload,
int? notificationId,
})
Scheduling
Future<bool> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? channelId,
Map<String, dynamic>? payload,
List<NotificationAction>? actions,
bool allowWhileIdle = false,
})
Future<bool> scheduleRecurringNotification({
required int id,
required String title,
required String body,
required String repeatInterval,
required int hour,
required int minute,
String? channelId,
Map<String, dynamic>? payload,
List<NotificationAction>? actions,
})
Background Handling & Diagnostics
Future<void> configureBackgroundMessageHandler(
Future<void> Function(RemoteMessage message) handler,
)
static Future<void> handleBackgroundMessage(RemoteMessage message)
Future<NotificationDiagnosticsResult> runDiagnostics()
Future<void> setUnifiedMessageHandler(
Future<bool> Function(NormalizedMessage message, NotificationLifecycle lifecycle) handler,
)
Inbox Storage
fetch({int page = 0, int pageSize = 20})βFuture<List<NotificationInboxItem>>upsert(NotificationInboxItem item)upsertAll(List<NotificationInboxItem> items)markRead(List<String> ids, {bool isRead = true})delete(List<String> ids)clear()count({bool unreadOnly = false})
Implementations:
InboxStorageServiceβ SharedPreferences-backed persistence.InMemoryInboxStorageβ memory-only, ideal for tests.
Badge Management
Future<void> setIOSBadgeCount(int count)
Future<int> getIOSBadgeCount()
Future<void> setAndroidBadgeCount(int count)
Future<int> getAndroidBadgeCount()
Future<void> clearBadgeCount()
Token Management
Future<String?> getFcmToken()
Future<void> clearToken()
Future<void> subscribeToTopic(String topic)
Future<void> unsubscribeFromTopic(String topic)
Future<void> unsubscribeFromAllTopics()
Analytics
void setAnalyticsCallback(Function(String, Map<String, dynamic>) callback)
void trackAnalyticsEvent(String event, Map<String, dynamic> data)
Data Models #
NotificationData
class NotificationData {
final Map<String, dynamic> payload;
final String? title;
final String? body;
final String? imageUrl;
final String? icon;
final String? category;
final List<NotificationAction>? actions;
final DateTime? timestamp;
final NotificationTypeEnum type;
final bool isFromTerminated;
final String? messageId;
final String? senderId;
final int? badgeCount;
}
NotificationAction
class NotificationAction {
final String id;
final String title;
final bool destructive;
final Map<String, dynamic>? payload;
}
NotificationChannelData
class NotificationChannelData {
final String id;
final String name;
final String? description;
final String? groupId;
final NotificationImportanceEnum importance;
final bool playSound;
final String? soundPath;
final String? soundFileName;
final bool enableVibration;
final bool enableLights;
final Int64List? vibrationPattern;
final Color? ledColor;
final bool showBadge;
final NotificationPriorityEnum priority;
final List<NotificationAction>? actions;
}
π§ Configuration #
Notification Channels #
Create custom notification channels for different types of notifications:
final List<NotificationChannelData> channels = [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: true,
),
NotificationChannelData(
id: 'silent_channel',
name: 'Silent Notifications',
description: 'Silent notification channel',
importance: NotificationImportanceEnum.low,
priority: NotificationPriorityEnum.low,
playSound: false,
enableVibration: false,
enableLights: false,
),
];
Platform-Specific Settings #
Android
NotificationChannelData(
id: 'android_channel',
name: 'Android Notifications',
description: 'Android-specific notifications',
importance: NotificationImportanceEnum.max,
priority: NotificationPriorityEnum.max,
playSound: true,
enableVibration: true,
enableLights: true,
vibrationPattern: Int64List.fromList([0, 1000, 500, 1000]),
ledColor: Color(0xFFFF0000),
showBadge: true,
)
iOS
NotificationChannelData(
id: 'ios_channel',
name: 'iOS Notifications',
description: 'iOS-specific notifications',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: false,
showBadge: true,
)
π Troubleshooting #
Common Issues #
Notifications not showing:
- Check Firebase configuration files are in place
- Verify sender ID is correct
- Check AndroidManifest.xml permissions
- Ensure notification channels are created
Scheduled notifications not working:
- Android 12+ (API 31+): Add
SCHEDULE_EXACT_ALARMandUSE_EXACT_ALARMpermissions - Android 13+ (API 33+): Request
SCHEDULE_EXACT_ALARMpermission at runtime - Ensure broadcast receivers are added to AndroidManifest.xml
- Check scheduled time is in the future
- Verify notification permissions are granted
π§ Android 13+ Runtime Permission:
For Android 13+ devices, you need to request the exact alarm permission at runtime:
import 'package:permission_handler/permission_handler.dart';
// Request exact alarm permission (Android 13+)
if (Platform.isAndroid) {
final status = await Permission.scheduleExactAlarm.request();
if (status.isGranted) {
// Permission granted, you can schedule notifications
} else {
// Permission denied, handle gracefully
print('Exact alarm permission denied');
}
}
Permission-related issues:
β "Exact alarms are not permitted"
- Cause: Missing
SCHEDULE_EXACT_ALARMpermission - Fix: Add permission to AndroidManifest.xml
- Alternative: Use
scheduleNotification()without exact timing
β "No push notifications received"
- Cause: Missing
INTERNETorWAKE_LOCKpermission - Fix: Add basic permissions to AndroidManifest.xml
β "Notifications don't vibrate"
- Cause: Missing
VIBRATEpermission - Fix: Add
VIBRATEpermission to AndroidManifest.xml
β "Foreground notifications not showing"
- Cause: Missing
FOREGROUND_SERVICEpermission - Fix: Add
FOREGROUND_SERVICEpermission to AndroidManifest.xml
iOS badges not updating:
- Requires proper APNs certificate configuration
- May not work in simulator
- Must upload APNs key to Firebase Console
APNs token not set error:
- This is NORMAL until APNs is configured
- Generate APNs key in Apple Developer Console
- Upload
.p8key file to Firebase Console - Choose correct environment (Sandbox/Production)
- This is a Firebase requirement, not a plugin issue
Custom sounds not playing:
- Add sound files to correct platform directories
- Create notification channels before using sounds
- Check file permissions and formats
Analytics not tracking:
- Ensure analytics callback is set
- Check event names and data format
- Verify analytics service integration
Debug Mode #
Enable debug mode for detailed logging:
// The plugin automatically logs detailed information in debug mode
// Check console output for initialization and operation logs
Error Handling #
The plugin provides comprehensive error handling:
try {
await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: channels,
androidNotificationIconPath: '@drawable/ic_notification',
);
} catch (e) {
print('Initialization failed: $e');
// Handle error appropriately
}
π€ Contributing #
We welcome contributions! Please see our Contributing Guide for details.
Development Setup #
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
Code Style #
- Follow Dart/Flutter conventions
- Add comprehensive documentation
- Include unit tests
- Ensure backward compatibility
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π Support #
- Documentation: Complete API Reference
- Examples: Example App β guided FCM showcase experience
- Issues: GitHub Issues
π What's Next? #
- In-App UX Kit v1 β survey carousel, tooltip, edge-to-edge banner, HTML modal with safe defaults.
- Notification Inbox Widget β themable list with read/delete and storage abstraction.
- Unified Handler + Schema Guard β single callback across lifecycles with strict data-only validator.
- Permission Wizard & Quiet Hours β guided POST_NOTIFICATIONS/exact alarm/APNs readiness plus defer/cap policies.
- Web Polish β permission overlay, custom icons/badges, service worker validator with actionable logs.
- Journeys & Server Recipes β ready payloads (welcome, win-back, NPS) plus Cloud Functions/REST examples under
server_recipes/. - Example App Upgrades β Notification Doctor tab, payload simulator, inbox + in-app demos.
Made with β€οΈ for the Flutter community