Push Notification Tracking
Complete guide to setting up push notification tracking in your Flutter app with the OpenCDP SDK.
Overview
The OpenCDP Flutter SDK provides comprehensive push notification tracking for:
- ✅ Notification delivered events (foreground, background, terminated states)
- ✅ Notification opened events
- ✅ iOS background delivery via Notification Service Extension
- ✅ Android FCM integration with automatic tracking
Prerequisites
Before setting up push notifications, ensure you have:
- Completed Installation and Quick Start
- Added
firebase_coreandfirebase_messagingto yourpubspec.yaml - Configured Firebase for your iOS and Android apps
- Created an iOS App Group (for iOS background tracking)
Setup Overview
Push notification setup requires configuration in three areas:
- Dart code - Push handlers and listeners
- Android - No additional native configuration needed
- iOS Native - Notification Service Extension setup
1. Dart Setup
Step 1: Create Push Service File
Create a new file lib/push_service.dart to hold your push handler logic:
// lib/push_service.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:open_cdp_flutter_sdk/open_cdp_flutter_sdk.dart';
import 'package:firebase_core/firebase_core.dart';
// This handler MUST be a top-level function
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Initialize Firebase for the background isolate
await Firebase.initializeApp();
// Let the SDK handle the background delivery event
OpenCDPSDK.handleBackgroundPushDelivery(message.data);
}
class PushService {
static Future<void> setupPushListeners() async {
// 1. Set the background message handler
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
// 2. Handles "delivered" events when the app is in the foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
OpenCDPSDK.handleForegroundPushDelivery(message.data);
});
// 3. Handles "opened" events if the app is opened from a terminated state
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
OpenCDPSDK.handlePushNotificationOpen(message.data);
}
});
// 4. Handles "opened" events if the app is opened from a background state
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
OpenCDPSDK.handlePushNotificationOpen(message.data);
});
}
}
The firebaseMessagingBackgroundHandler must be a top-level function (not inside a class). This is required by Flutter's background isolate system.
Step 2: Call in main.dart
In your lib/main.dart, import and call the push service setup after SDK initialization:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:open_cdp_flutter_sdk/open_cdp_flutter_sdk.dart';
// Import your push service
import 'push_service.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Initialize OpenCDP SDK
await OpenCDPSDK.initialize(
config: OpenCDPConfig(
cdpApiKey: 'your-api-key',
iOSAppGroup: 'group.com.yourcompany.yourapp',
// ... other config
),
);
// Set up push listeners AFTER SDK initialization
await PushService.setupPushListeners();
runApp(MyApp());
}
2. Android Setup
No additional native configuration is required for Android. The Dart code is sufficient for full push notification tracking.
Firebase Cloud Messaging (FCM) is automatically configured through the google-services.json file in your Android project.
3. iOS Native Setup
iOS requires additional native configuration to track notification delivery when your app is in the background or terminated.
Step 1: Open Xcode
Open your project's iOS workspace:
cd ios
open Runner.xcworkspace
Step 2: Add Notification Service Extension
- In Xcode, go to File > New > Target...
- Select Notification Service Extension and click Next
- Enter a name (e.g.,
NotificationService) and click Finish - When prompted to activate the new scheme, click Do not Activate
Step 3: Add App Group
The App Group allows the extension and main app to share data.
For Main App (Runner target):
- Select the Runner target in Xcode
- Go to the Signing & Capabilities tab
- Click + Capability and add App Groups
- Click the + button in the App Groups section
- Add a new group with ID:
group.com.yourcompany.yourapp(This must match theiOSAppGroupin your Dart config)
For Notification Service Extension:
- Select your NotificationService extension target
- Repeat the exact same steps above
- Important: Select the exact same App Group ID
The App Group ID must be:
- Identical in both targets (Runner and NotificationService)
- Identical to the
iOSAppGroupin your Dart configuration - Unique to your organization
- In the format:
group.com.yourcompany.yourapp
Step 4: Add Code to Extension
In the Xcode project navigator, find the NotificationService folder and open NotificationService.swift.
Replace its entire contents with this code:
// In NotificationService/NotificationService.swift
import UserNotifications
import open_cdp_flutter_sdk // Import the SDK's native package
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
// !!! IMPORTANT: Paste your App Group ID here !!!
let appGroup = "group.com.yourcompany.yourapp"
// Pass the request to the OpenCDP SDK Handler
OpenCdpPushExtensionHelper.didReceiveNotificationExtensionRequest(
request,
appGroup: appGroup
) { modifiedContent in
contentHandler(modifiedContent)
}
}
override func serviceExtensionTimeWillExpire() {
// Called if the SDK's processing takes too long.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
Make sure to replace "group.com.yourcompany.yourapp" with your actual App Group ID!
Step 5: Configure Podfile
Open the ios/Podfile in your code editor and add the new extension target.
Your Podfile should look similar to this:
# In ios/Podfile
# ... (existing content like platform and project setup) ...
target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# Link the plugin to the main Runner target
pod 'open_cdp_flutter_sdk', :path => '.symlinks/plugins/open_cdp_flutter_sdk/ios'
end
# === ADD THIS NEW TARGET ===
target 'NotificationService' do
inherit! :search_paths
use_modular_headers!
# Link the SDK's native code to the extension
pod 'open_cdp_flutter_sdk', :path => '.symlinks/plugins/open_cdp_flutter_sdk/ios'
end
post_install do |installer|
# ... (existing content) ...
end
Step 6: Install Pods
Run pod install from your ios directory:
cd ios
pod install
cd ..
4. Actionable Notifications (Manual Mode)
Use this section when your push payload includes data.actions.
For now, iOS action registration is manual:
- App teams maintain a fixed allowlist of
action_idvalues. - Those ids must be registered in native iOS code.
- Campaign authors should only use ids on that allowlist.
iOS: Register CDP_ACTIONS Category at Launch
action_id in CDP must match UNNotificationAction.identifier.
import UIKit
import Flutter
import UserNotifications
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
registerCDPActionCategory()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func registerCDPActionCategory() {
let openAction = UNNotificationAction(
identifier: "view_offer", // MUST match CDP action_id
title: "View Offer",
options: [.foreground]
)
let dismissAction = UNNotificationAction(
identifier: "dismiss_offer", // MUST match CDP action_id
title: "Dismiss",
options: []
)
let category = UNNotificationCategory(
identifier: "CDP_ACTIONS",
actions: [openAction, dismissAction],
intentIdentifiers: [],
options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([category])
}
}
iOS: Action Tap Handling
Forward action taps so your app can use tapped action_id for routing/UX:
import UserNotifications
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let actionId = response.actionIdentifier
if actionId != UNNotificationDefaultActionIdentifier &&
actionId != UNNotificationDismissActionIdentifier {
// Forward to Flutter side if needed and pass action_id in payload data.
}
completionHandler()
}
}
Android: Render Actions for Background/Terminated
When actions are present, Android push is data-focused. Use SDK helper rendering in your background handler:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:open_cdp_flutter_sdk/open_cdp_flutter_sdk.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
// 1) Track delivery
await OpenCDPSDK.handleBackgroundPushDelivery(message.data);
// 2) Render native Android notification + actions
await OpenCDPSDK.showAndroidActionableNotification(
message.data,
channelName: 'CDP Notifications',
channelDescription: 'Push notifications from CDP',
);
}
Optional Flutter Tap Callback (for custom local notifications)
import 'dart:convert';
Future<void> onLocalNotificationTap(NotificationResponse response) async {
final payload = response.payload;
if (payload == null || payload.isEmpty) return;
final data = Map<String, dynamic>.from(jsonDecode(payload));
final actionId = response.actionId;
if (actionId != null && actionId.isNotEmpty) {
data['action_id'] = actionId;
}
await OpenCDPSDK.handlePushNotificationOpen(data);
}
How It Works
Event Types
The SDK automatically tracks three types of push notification events:
-
Delivered - Notification was delivered to the device
- Foreground: App is open and active
- Background: App is backgrounded
- Terminated: App is not running
-
Opened - User tapped the notification
- From background state
- From terminated state
Event Handlers
| Handler Method | When Called | Platform |
|---|---|---|
handleForegroundPushDelivery() | Notification received while app is in foreground | iOS, Android |
handleBackgroundPushDelivery() | Notification received while app is in background or terminated | iOS, Android |
handlePushNotificationOpen() | User taps notification | iOS, Android |
Testing
Test Foreground Delivery
- Ensure your app is open and in the foreground
- Send a test notification from Firebase Console
- Check debug logs for tracking confirmation
Test Background Delivery
- Background your app (don't close it)
- Send a test notification
- Check that the background handler was called
Test Notification Open
- Send a notification (app backgrounded or closed)
- Tap the notification
- Verify the open event was tracked
Troubleshooting
iOS: Notifications Not Tracking in Background
- ✅ Verify App Group ID matches in all three places:
- Dart configuration (
iOSAppGroup) - Runner target capabilities
- NotificationService target capabilities
- Dart configuration (
- ✅ Verify
NotificationService.swifthas correct App Group ID - ✅ Verify Podfile links SDK to extension target
- ✅ Run
pod installafter Podfile changes
Android: Background Handler Not Called
- ✅ Verify
@pragma('vm:entry-point')annotation on handler function - ✅ Verify handler is a top-level function (not in a class)
- ✅ Verify Firebase is initialized in the background handler
Events Not Appearing in Dashboard
- ✅ Verify user is identified before sending notifications
- ✅ Check debug logs for error messages
- ✅ Verify API key is correct
- ✅ Ensure notification payload includes required data
Best Practices
1. Register Device Token
Always register the device token after user identification:
// After user logs in
await OpenCDPSDK.instance.identify(
identifier: userId,
properties: {'email': email},
);
// Register device for push
final fcmToken = await FirebaseMessaging.instance.getToken();
await OpenCDPSDK.instance.registerDeviceToken(
fcmToken: fcmToken, // Android
apnToken: fcmToken, // iOS (FCM handles conversion)
);
2. Handle Token Refresh
Listen for token updates:
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
OpenCDPSDK.instance.registerDeviceToken(
fcmToken: newToken,
apnToken: newToken,
);
});
3. Test on Real Devices
Always test push notifications on real devices, not simulators/emulators:
- iOS Simulator cannot receive push notifications
- Android Emulator requires special setup