Zixflow Flutter SDK - Integration Guide
Complete guide for integrating the Zixflow Flutter SDK into your application.
Table of Contents
- Introduction
- Requirements
- Installation
- Quick Start
- Core Features
- Push Notifications
- In-App Messaging
- Location Tracking
- Advanced Configuration
- Platform-Specific Setup
- Troubleshooting
- Example Apps
- API Reference
Introduction
The Zixflow Flutter SDK enables you to send data from your Flutter app to Zixflow, allowing you to track user behavior, send targeted push notifications, display in-app messages, and more. The SDK works across iOS and Android platforms.
SDK Modules
The SDK includes integrated modules:
- Data Pipelines (core): User identification, event tracking, and analytics
- Push Messaging: Push notifications via Firebase Cloud Messaging (FCM)
- In-App Messaging: Display in-app messages and manage inbox
- Location Tracking: Location-based messaging capabilities
Requirements
- Flutter: 2.5.0 or higher
- Dart: 2.17.6 or higher
- iOS: 13.0 or higher
- Android: API 21 (Android 5.0 Lollipop) or higher
Installation
The Zixflow Flutter SDK is available on pub.dev. The latest version is 1.1.5.
Add Dependency
Add the Zixflow SDK to your pubspec.yaml:
dependencies:
zixflow: ^1.1.5
Install Package
Run the following command:
flutter pub get
Platform-Specific Setup
After adding the dependency, follow platform-specific setup for iOS and Android (see Platform-Specific Setup section).
Quick Start
1. Get Your API Key
Your API key can be found in your Zixflow account:
- Log in to your Zixflow account
- Go to Settings → Developers → API Keys
- Copy your API key
2. Basic Initialization
Create or update your main.dart:
import 'package:flutter/material.dart';
import 'package:zixflow/zixflow.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Zixflow SDK
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
logLevel: LogLevel.debug,
);
await Zixflow.initialize(config: config);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: HomeScreen(),
);
}
}
Replace YOUR_API_KEY with your actual API key from the Zixflow dashboard.
Core Features
User Identification
Identify users to track their activity:
// Identify user with ID only
Zixflow.instance.identify(userId: 'user@example.com');
// Identify user with additional attributes
Zixflow.instance.identify(
userId: 'user@example.com',
traits: {
'first_name': 'John',
'last_name': 'Doe',
'email': 'user@example.com',
'plan': 'premium',
'created_at': DateTime.now().toIso8601String(),
},
);
Event Tracking
Track custom events:
// Simple event
Zixflow.instance.track(name: 'button_clicked');
// Event with properties
Zixflow.instance.track(
name: 'purchase_completed',
properties: {
'product_id': '123',
'product_name': 'Widget',
'price': 29.99,
'currency': 'USD',
},
);
// Event with timestamp
Zixflow.instance.track(
name: 'order_completed',
properties: {'order_id': 'ABC123'},
timestamp: DateTime.now().millisecondsSinceEpoch,
);
Screen Tracking
Track screen views:
// Manual screen tracking
Zixflow.instance.screen(
title: 'Product Detail',
properties: {
'product_id': '123',
'category': 'Electronics',
},
);
Automatic Screen Tracking with GoRouter
Enable automatic screen tracking by setting screenViewUse in configuration:
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
screenViewUse: ScreenView.all, // Track all screens automatically
);
Profile Attributes
Set user profile attributes:
// Set profile attributes
Zixflow.instance.setProfileAttributes(
attributes: {
'age': 30,
'gender': 'male',
'subscription_status': 'active',
'preferences': {'notifications': true},
},
);
Device Attributes
Set device-specific attributes:
// Set device attributes
Zixflow.instance.setDeviceAttributes(
attributes: {
'app_theme': 'dark',
'notifications_enabled': true,
'preferred_language': 'en',
},
);
User Logout
Clear user identification when they log out:
// Clear identified user
Zixflow.instance.clearIdentify();
Register Device Token
Register device token for push notifications:
Zixflow.instance.registerDeviceToken(deviceToken: 'YOUR_DEVICE_TOKEN');
Push Notifications
Complete guide to implementing push notifications in Flutter with Zixflow using Firebase Cloud Messaging (FCM) for both iOS and Android.
💡 Track your campaigns: The SDK automatically tracks delivery, opens, and clicks. For advanced tracking customization and implementation details, see Push Notification Tracking.
Prerequisites
Before implementing push notifications:
- Configure FCM in Zixflow dashboard - Add FCM Server Key to Zixflow dashboard
- Firebase Project Setup - Create Firebase project and add both iOS and Android apps
- Device tokens must be associated with users - Call
identify()before sending notifications - Test on physical devices only - Emulators/simulators cannot receive push notifications
Important: A device/user can't receive push notifications until you: (1) Register a device token, (2) Identify a person, (3) Check notification permissions.
Platform-Specific Requirements
iOS Requirements
- iOS 13.0 or higher
- Xcode 14.0 or higher
- Push Notifications capability enabled in Xcode
- Notification Service Extension for rich push (images, videos)
- Physical Device required for testing
Android Requirements
- API Level 21 (Android 5.0) or higher
- Firebase Project configured
- google-services.json in
android/app/directory - POST_NOTIFICATIONS permission for Android 13+ (API 33+)
Step 1: Firebase Project Setup
1. Create Firebase Project
- Go to https://console.firebase.google.com
- Click Create a project or select existing project
- Follow the setup wizard
2. Add iOS App to Firebase
- In Firebase console, click Add app → Select iOS
- Enter iOS Bundle ID (e.g.,
com.yourcompany.app) - Download
GoogleService-Info.plist - Add
GoogleService-Info.plisttoios/Runner/directory in Xcode
3. Add Android App to Firebase
- In Firebase console, click Add app → Select Android
- Enter Android package name (same as iOS bundle ID)
- Download
google-services.json - Place
google-services.jsoninandroid/app/directory
Step 2: Add Dependencies
Update your pubspec.yaml:
dependencies:
zixflow: ^1.1.5
firebase_core: ^3.3.0
firebase_messaging: ^15.0.4
flutter_local_notifications: ^19.5.0 # For foreground notifications
Install dependencies:
flutter pub get
Step 3: Configure Flutter Code
Initialize Firebase and Zixflow
Update your main.dart:
import 'package:flutter/material.dart';
import 'package:zixflow/zixflow.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart'; // Generated by FlutterFire CLI
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase FIRST
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Configure foreground notification presentation (iOS)
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
// Initialize Zixflow SDK
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
logLevel: LogLevel.debug,
);
await Zixflow.initialize(config: config);
runApp(MyApp());
}
Note: To generate firebase_options.dart, use FlutterFire CLI:
# Install FlutterFire CLI
dart pub global activate flutterfire_cli
# Configure Firebase for your Flutter app
flutterfire configure
Step 4: Handle Push Notifications
Complete push notification handling implementation:
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';
// Background message handler (must be top-level function)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Handling background message: ${message.messageId}');
// Track delivery metric if needed
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_setupPushNotifications();
}
Future<void> _setupPushNotifications() async {
// Register background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Request permission (iOS)
await _requestPushPermission();
// Get FCM token and register with Zixflow
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
Zixflow.instance.registerDeviceToken(deviceToken: token);
print('FCM Token: $token');
}
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
Zixflow.instance.registerDeviceToken(deviceToken: newToken);
print('FCM Token refreshed: $newToken');
});
// Handle notification when app is opened from terminated state
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
_handlePushNotificationClick(message, 'terminated');
}
});
// Handle notification when app is in background
FirebaseMessaging.onMessageOpenedApp.listen((message) {
_handlePushNotificationClick(message, 'background');
});
// Handle notification when app is in foreground
FirebaseMessaging.onMessage.listen((message) {
print('Foreground message: ${message.notification?.title}');
_showForegroundNotification(message);
});
}
Future<void> _requestPushPermission() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
NotificationSettings settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted push notification permission');
} else if (settings.authorizationStatus == AuthorizationStatus.denied) {
print('User declined push notification permission');
} else {
print('Push notification permission not determined');
}
}
void _handlePushNotificationClick(RemoteMessage message, String appState) {
print('Push notification clicked - App state: $appState');
// Track push notification click with Zixflow
Zixflow.instance.track(
name: 'push_notification_clicked',
properties: {
'push_title': message.notification?.title ?? '',
'app_state': appState,
'message_id': message.messageId ?? '',
},
);
// Handle deep link navigation
if (message.data.containsKey('link')) {
final deepLink = message.data['link'];
_navigateToDeepLink(deepLink);
}
}
void _showForegroundNotification(RemoteMessage message) {
// Display notification when app is in foreground
// Use flutter_local_notifications package (see Local Notifications section)
}
void _navigateToDeepLink(String deepLink) {
// Implement deep link navigation
// Example: Navigate to specific screen based on deepLink
print('Navigating to deep link: $deepLink');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: HomeScreen(),
);
}
}
Step 5: Identify Users
Critical: Device tokens must be associated with a user before push notifications can be delivered.
import 'package:zixflow/zixflow.dart';
// Identify user when they log in
Future<void> handleLogin(String userId, String email) async {
try {
// Identify user with Zixflow
Zixflow.instance.identify(
userId: userId,
traits: {
'email': email,
'first_name': 'John',
'last_name': 'Doe',
'plan': 'premium',
},
);
print('User identified: $userId');
} catch (e) {
print('Failed to identify user: $e');
}
}
Important Notes:
- Device tokens are automatically generated when the app starts
- Tokens are linked to Zixflow profiles when you call
identify() - This ensures notifications are delivered to the correct user
iOS-Specific Setup
Step 1: Enable Push Capabilities in Xcode
- Open
<yourAppName>.xcworkspaceinios/folder - Select your main app target (Runner)
- Go to Signing & Capabilities tab
- Click + Capability → Add Push Notifications
- Click + Capability → Add Background Modes
- Enable Remote notifications under Background Modes
Step 2: Create Notification Service Extension
For rich push notifications (images, videos, GIFs):
1. Create Extension in Xcode:
- Select File → New → Target
- Choose Notification Service Extension
- Name it
NotificationServiceExtension - Click Finish
- Click Cancel when asked about activation
2. Configure Podfile:
Update ios/Podfile:
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# Add Zixflow FCM pod
pod 'zixflow/fcm', :path => '.symlinks/plugins/zixflow/ios'
end
# Notification Service Extension target
target 'NotificationServiceExtension' do
use_frameworks!
pod 'zixflow_richpush/fcm', :path => '.symlinks/plugins/zixflow/ios'
end
For Swift Package Manager (Flutter 3.24+, Xcode 16.0+):
Enable SPM:
flutter config --enable-swift-package-manager
Remove old pod references from Podfile, then in Xcode:
- Add
FlutterGeneratedPluginSwiftPackageto NotificationServiceExtension target - Go to General → Frameworks and Libraries
3. Install Dependencies:
cd ios
pod install --repo-update
cd ..
4. Update NotificationService.swift:
import UserNotifications
import ZixflowMessagingPushFCM
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
// Initialize SDK for extension
MessagingPushFCM.initializeForExtension(
withConfig: MessagingPushConfigBuilder(apiKey: "YOUR_API_KEY")
.build()
)
if let bestAttemptContent = bestAttemptContent {
// Let Zixflow handle rich push and track delivery
MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
}
}
override func serviceExtensionTimeWillExpire() {
// Called when extension is about to be terminated
MessagingPush.shared.serviceExtensionTimeWillExpire()
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
Important: Replace "YOUR_API_KEY" with your actual API key.
Step 3: Configure AppDelegate.swift
Update ios/Runner/AppDelegate.swift:
import UIKit
import Flutter
import ZixflowMessagingPushFCM
import ZixflowFirebaseWrapper
import FirebaseMessaging
import FirebaseCore
@main
class AppDelegateWitZfIntegration: ZixflowAppDelegateWrapper<AppDelegate> {}
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Initialize Zixflow push messaging
MessagingPushFCM.initialize(
withConfig: MessagingPushConfigBuilder()
.showPushAppInForeground(true) // Show notifications in foreground
.build()
)
// Set notification delegate
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
// Register APNs token with Firebase
Messaging.messaging().apnsToken = deviceToken
}
// Optional: Fine-grained control over foreground presentation
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .list, .badge, .sound])
}
}
Foreground Notification Options:
- Set
showPushAppInForeground(true)to display notifications when app is active - Customize presentation with
userNotificationCenter:willPresent:delegate method - Available options:
.banner,.list,.badge,.sound
Sound in Notifications:
- Your app needs permission to play sounds (requested via
requestPermission) - Users can disable sound in iOS Settings
- Use "Default" sound setting for best experience (triggers vibration when sound is disabled)
- Custom sounds require additional native configuration
Android-Specific Setup
Android push is automatically configured when following the Getting Started instructions. Additional customization is available:
Step 1: Configure Gradle
Project-level android/build.gradle:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
App-level android/app/build.gradle:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.gms.google-services' // Add this line
}
android {
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
}
}
Step 2: Add Permissions
Add to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required for Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- Your app configuration -->
</application>
</manifest>
Step 3: Customize Notification Icon and Color
Add to android/app/src/main/AndroidManifest.xml inside <application> tag:
<application>
<!-- Default notification icon (white, transparent background) -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<!-- Default notification color (accent color) -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorNotificationIcon" />
</application>
Create notification icon:
- Place white icon PNG in
android/app/src/main/res/drawable/ic_notification.png - Create multiple sizes for different densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
Define notification color:
Add to android/app/src/main/res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorNotificationIcon">#FF6200EE</color>
</resources>
Local Notifications (Foreground Display)
Show notifications when app is in foreground using flutter_local_notifications:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
final FlutterLocalNotificationsPlugin localNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> setupLocalNotifications() async {
// Android settings
const androidSettings = AndroidInitializationSettings('@drawable/ic_notification');
// iOS settings
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
// Initialization settings
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await localNotificationsPlugin.initialize(
initSettings,
onDidReceiveNotificationResponse: (response) {
// Handle local notification click
Zixflow.instance.track(
name: 'local_notification_clicked',
properties: {'payload': response.payload ?? ''},
);
},
);
}
Future<void> showForegroundNotification(RemoteMessage message) async {
const androidDetails = AndroidNotificationDetails(
'default_channel',
'Default Channel',
channelDescription: 'Default notification channel',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await localNotificationsPlugin.show(
message.hashCode,
message.notification?.title ?? 'New Message',
message.notification?.body ?? '',
details,
payload: message.data['link'],
);
}
Rich Push Payload Structure
Zixflow automatically handles rich push notifications. Here's how to structure payloads:
iOS FCM Payload
{
"message": {
"apns": {
"payload": {
"aps": {
"mutable-content": 1,
"alert": {
"title": "Special Offer",
"body": "Check out our new features!"
},
"badge": 1,
"sound": "default"
},
"zixflow": {
"push": {
"link": "yourapp://product/123",
"image": "https://example.com/image.jpg"
}
}
},
"headers": {
"apns-priority": 10
}
}
}
}
Android Payload
{
"message": {
"data": {
"title": "Order Shipped",
"body": "Your order is on the way!",
"image": "https://example.com/shipping.jpg",
"link": "yourapp://orders/456"
}
}
}
Testing Push Notifications
1. Verify Device Token Registration
Check if device token is registered:
Future<void> verifyPushSetup() async {
try {
final token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
if (token != null) {
print('✓ Device token registered');
} else {
print('✗ Device token not available');
}
final settings = await FirebaseMessaging.instance.getNotificationSettings();
print('Permission status: ${settings.authorizationStatus}');
} catch (e) {
print('Push verification failed: $e');
}
}
2. Enable Debug Logging
Set log level to debug during development:
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
logLevel: LogLevel.debug, // Verbose logging
);
3. Test on Physical Devices
iOS:
- Push notifications do not work on iOS Simulator
- Must use physical iOS device
- Ensure APNs certificate configured in Firebase
Android:
- Emulator works if Google Play Services installed
- Physical device always recommended
- Ensure FCM Server Key configured in Zixflow dashboard
4. Send Test Push from Zixflow
- Log into Zixflow dashboard
- Navigate to Messaging → Push Notifications
- Create test push notification with image
- Send to specific user (using
userIdfromidentify())
Troubleshooting
Firebase Configuration Issues
Error: GoogleService-Info.plist not found (iOS)
Solution:
- Ensure file is in
ios/Runner/directory - Verify file is added to Xcode target (check target membership)
- Clean and rebuild:
flutter clean && flutter pub get
Error: google-services.json not found (Android)
Solution:
- Ensure file is in
android/app/directory - Verify Google Services plugin applied in
android/app/build.gradle - Sync Gradle files
Push Notifications Not Received
iOS Checklist:
Push Notifications capability enabled in XcodeAPNs certificate uploaded to FirebaseTesting on physical device (not simulator)User identified:Zixflow.instance.identify()Permission granted by userForeground display configured if app is active
Android Checklist:
google-services.jsoninandroid/app/directoryGoogle Services plugin appliedFCM Server Key in Zixflow dashboardPOST_NOTIFICATIONS permission granted (Android 13+)Device registered for push
Rich Push Not Working (Images)
iOS Solutions:
- Ensure Notification Service Extension properly configured
- Verify extension has correct dependencies in Podfile
- Check
mutable-content: 1in push payload - Verify image URL is accessible
- Check extension logs for errors
Android Solutions:
- Verify image URL accessible from device
- Check internet connectivity
- Ensure image format supported (JPEG, PNG, GIF)
Permission Denied
Symptoms: User denied push permission
Solutions:
- Explain value before requesting permission
- Show in-app message about benefits
- Guide users to Settings to enable manually:
import 'package:permission_handler/permission_handler.dart'; if (await Permission.notification.isDenied) { openAppSettings(); }
Device Token Not Available
Symptoms: getToken() returns null
Solutions:
- Verify Firebase initialized before calling
getToken() - Check Google Play Services available (Android)
- Ensure internet connectivity
- Review native logs for errors
- Rebuild app and retry
Best Practices
- Initialize Firebase first - Before Zixflow SDK
- Request permission thoughtfully - Explain value before asking
- Identify users immediately - After authentication
- Test on physical devices - Simulators have limitations
- Use rich push - Images increase engagement
- Handle deep links - Provide seamless navigation
- Monitor metrics - Track delivery and open rates
- Enable debug logging - During development only
- Test both platforms - iOS and Android behave differently
- Clear identity on logout - Call
clearIdentify() - Configure App Groups (iOS) - For reliable delivery tracking
Push Notification Tracking
Overview
Zixflow automatically tracks push notification delivery and engagement metrics to help you measure campaign performance. The SDK tracks three key lifecycle events:
- Delivery Confirmed - When the notification arrives on the device
- Notification Opened - When the user taps the notification
- Action Button Clicked - When the user taps an action button
How Push Tracking Works
When Zixflow sends a push notification, it includes special tracking fields in the data payload:
| Field | Purpose |
|---|---|
Zixflow-Delivery-ID |
Unique ID for this delivery (links to campaign record) |
Zixflow-Delivery-Token |
The FCM/APNs token the notification was sent to |
Example push payload:
{
"Zixflow-Delivery-ID": "626533406292836846",
"Zixflow-Delivery-Token": "dcFRlDhiRbehM1vg...",
"title": "Flash Sale! 70% OFF",
"body": "Limited time only — grab your deal now!",
"deeplink_url": "https://yourapp.com/sale",
"image_url": "https://cdn.yourapp.com/banner.png",
"action_buttons": "[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
}
Note: The SDK automatically handles most tracking for you when using the default push implementation. This section covers advanced customization scenarios.
Tracking Events
1. Delivery Confirmed
When to track: The moment the push payload arrives on the device
SDK method: Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.delivered)
Implementation:
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';
// Listen for foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? cachedFcmToken ?? '';
if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
Zixflow.instance.trackMetric(
deliveryID: deliveryId,
deviceToken: deliveryToken,
event: MetricEvent.delivered,
);
}
});
Important: The Zixflow SDK automatically tracks delivery. Manual tracking is only needed for custom implementations.
2. Notification Opened
When to track: When the user taps the notification banner (body or action button)
SDK method: Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.opened)
Implementation:
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';
// Background tap — app was backgrounded
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_trackOpened(message.data, cachedFcmToken);
_handleDeeplink(message.data['deeplink_url']);
});
// Terminated tap — app was closed when notification was tapped
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
_trackOpened(initial.data, cachedFcmToken);
_handleDeeplink(initial.data['deeplink_url']);
}
void _trackOpened(Map<String, String> data, String? fallbackToken) {
final deliveryId = data['Zixflow-Delivery-ID'] ?? '';
final deliveryToken = data['Zixflow-Delivery-Token'] ?? fallbackToken ?? '';
if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
Zixflow.instance.trackMetric(
deliveryID: deliveryId,
deviceToken: deliveryToken,
event: MetricEvent.opened,
);
}
}
Note: The SDK automatically tracks opens when using the default push implementation.
3. Action Button Clicked
When to track: When the user taps a named action button
SDK method: Zixflow.instance.track(name: "Push Notification Action Clicked", properties: {...})
Implementation (using flutter_local_notifications):
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';
// In FlutterLocalNotificationsPlugin.initialize()
onDidReceiveNotificationResponse: (NotificationResponse response) {
final payload = jsonDecode(response.payload ?? '{}') as Map<String, dynamic>;
if (response.actionId == null) {
// Body tap → opened
_trackOpened(payload, null);
} else {
// Action button tap → opened + clicked
_trackOpened(payload, null);
final actionIndex = int.tryParse(
response.actionId!.replaceAll(RegExp(r'\D'), '')) ?? -1;
final buttons = _parseButtons(payload['action_buttons']);
final buttonName = (actionIndex >= 0 && actionIndex < buttons.length)
? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
: 'Action ${actionIndex + 1}';
Zixflow.instance.track(
name: 'Push Notification Action Clicked',
properties: {
'Zixflow-Delivery-ID': payload['Zixflow-Delivery-ID'] ?? '',
'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
'notification_id': payload['Zixflow-Delivery-ID'] ?? '',
'title': payload['title'] ?? '',
'action_id': response.actionId!,
'action_index': actionIndex,
'action_name': buttonName,
'action_deeplink': _resolveButtonDeeplink(buttons, actionIndex),
'source': 'local_notification',
},
);
}
},
List<Map<String, dynamic>> _parseButtons(dynamic raw) {
if (raw == null) return [];
try {
final decoded = raw is String ? jsonDecode(raw) : raw;
return List<Map<String, dynamic>>.from(decoded as List);
} catch (_) {
return [];
}
}
Action button properties:
| Property | Type | Required | Description |
|---|---|---|---|
Zixflow-Delivery-ID |
string | ✅ | Links event to campaign delivery |
action_index |
integer | ✅ | 0-based index of button tapped |
action_name |
string | ✅ | Button label (e.g., "Shop Now") |
Zixflow-Delivery-Token |
string | Recommended | FCM/APNs token |
action_deeplink |
string | Recommended | Button's URL |
Action Buttons Format
The action_buttons field is a JSON-encoded string containing button definitions:
Raw payload value:
"[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
Parsed structure:
[
{ "name": "Shop Now", "deeplink": "https://yourapp.com/sale" },
{ "name": "Remind Me", "deeplink": "" }
]
Parsing example:
import 'dart:convert';
List<Map<String, dynamic>> parseActionButtons(dynamic raw) {
if (raw == null) return [];
try {
final decoded = raw is String ? jsonDecode(raw) : raw;
return List<Map<String, dynamic>>.from(decoded as List);
} catch (e) {
print('Error parsing action buttons: $e');
return [];
}
}
// Usage
final buttons = parseActionButtons(message.data['action_buttons']);
for (var i = 0; i < buttons.length; i++) {
print('Button ${i}: ${buttons[i]['name']} -> ${buttons[i]['deeplink']}');
}
Rules:
- Maximum 2 buttons per notification (recommended for consistency across platforms)
deeplinkmay be empty — handle gracefully- Button index is 0-based (first button = index 0)
Complete Push Tracking Implementation
Here's a complete example integrating all tracking events:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';
// Background message handler (top-level function)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
// Track delivery
final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? '';
if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
Zixflow.instance.trackMetric(
deliveryID: deliveryId,
deviceToken: deliveryToken,
event: MetricEvent.delivered,
);
}
}
class PushNotificationHandler {
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
String? _fcmToken;
Future<void> initialize() async {
// Register background handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Get and cache FCM token
_fcmToken = await FirebaseMessaging.instance.getToken();
if (_fcmToken != null) {
Zixflow.instance.registerDeviceToken(deviceToken: _fcmToken!);
}
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
_fcmToken = newToken;
Zixflow.instance.registerDeviceToken(deviceToken: newToken);
});
// Initialize local notifications
await _initializeLocalNotifications();
// Set up message listeners
_setupMessageListeners();
}
Future<void> _initializeLocalNotifications() async {
const androidSettings = AndroidInitializationSettings('@drawable/ic_notification');
const iosSettings = DarwinInitializationSettings();
await _localNotifications.initialize(
InitializationSettings(android: androidSettings, iOS: iosSettings),
onDidReceiveNotificationResponse: _handleNotificationResponse,
);
}
void _setupMessageListeners() {
// Foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_trackDelivery(message);
_showLocalNotification(message);
});
// Background tap
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_trackOpened(message.data);
_handleDeeplink(message.data['deeplink_url']);
});
// Terminated tap
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
_trackOpened(message.data);
_handleDeeplink(message.data['deeplink_url']);
}
});
}
void _trackDelivery(RemoteMessage message) {
final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';
if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
Zixflow.instance.trackMetric(
deliveryID: deliveryId,
deviceToken: deliveryToken,
event: MetricEvent.delivered,
);
}
}
void _trackOpened(Map<String, dynamic> data) {
final deliveryId = data['Zixflow-Delivery-ID'] ?? '';
final deliveryToken = data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';
if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
Zixflow.instance.trackMetric(
deliveryID: deliveryId,
deviceToken: deliveryToken,
event: MetricEvent.opened,
);
}
}
void _handleNotificationResponse(NotificationResponse response) {
if (response.payload == null) return;
final payload = jsonDecode(response.payload!) as Map<String, dynamic>;
// Track opened
_trackOpened(payload);
// If action button was tapped
if (response.actionId != null) {
_trackActionClick(payload, response.actionId!);
}
// Handle navigation
_handleDeeplink(payload['deeplink_url']);
}
void _trackActionClick(Map<String, dynamic> payload, String actionId) {
final actionIndex = int.tryParse(actionId.replaceAll(RegExp(r'\D'), '')) ?? -1;
final buttons = _parseButtons(payload['action_buttons']);
final actionName = (actionIndex >= 0 && actionIndex < buttons.length)
? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
: 'Action ${actionIndex + 1}';
final actionDeeplink = (actionIndex >= 0 && actionIndex < buttons.length)
? buttons[actionIndex]['deeplink'] ?? ''
: '';
Zixflow.instance.track(
name: 'Push Notification Action Clicked',
properties: {
'Zixflow-Delivery-ID': payload['Zixflow-Delivery-ID'] ?? '',
'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
'action_index': actionIndex,
'action_name': actionName,
'action_deeplink': actionDeeplink,
},
);
}
List<Map<String, dynamic>> _parseButtons(dynamic raw) {
if (raw == null) return [];
try {
final decoded = raw is String ? jsonDecode(raw) : raw;
return List<Map<String, dynamic>>.from(decoded as List);
} catch (_) {
return [];
}
}
Future<void> _showLocalNotification(RemoteMessage message) async {
// Implementation for showing local notification
}
void _handleDeeplink(String? deeplink) {
if (deeplink == null || deeplink.isEmpty) return;
// Implement your deeplink navigation logic
print('Navigating to: $deeplink');
}
}
Tracking Decision Flowchart
Notification received?
├── YES → trackMetric(delivered) [automatic with SDK]
│
User interacted?
├── Tapped notification body
│ └── trackMetric(opened) [automatic with SDK]
│ └── Navigate to deeplink_url
│
└── Tapped action button
├── trackMetric(opened) [automatic with SDK]
└── track("Push Notification Action Clicked")
└── Navigate to button's deeplink
Fallback for Non-Zixflow Push
If a push notification doesn't contain Zixflow-Delivery-ID (sent from another source), the SDK automatically skips tracking.
For custom analytics on non-Zixflow pushes, use a different event name:
// Don't use reserved push event names for non-Zixflow pushes
Zixflow.instance.track(
name: 'External Push Received',
properties: {
'notification_id': message.messageId ?? '',
'title': message.notification?.title ?? message.data['title'] ?? '',
'body': message.notification?.body ?? message.data['body'] ?? '',
'source': 'external',
},
);
Important: Push notification event names (
Push Notification Delivered,Push Notification Opened,Push Notification Action Clicked) are reserved for Zixflow's delivery pipeline.
Testing Push Tracking
Verify Tracking Events
Enable debug logging to see tracking events:
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
logLevel: LogLevel.debug,
);
Look for these log messages:
[Zixflow] Push Notification Delivered tracked[Zixflow] Push Notification Opened tracked[Zixflow] Push Notification Action Clicked tracked
Check Campaign Analytics
- Log into Zixflow dashboard
- Navigate to Messaging → Push Notifications
- View campaign metrics:
- Sent - Total notifications sent
- Delivered - Arrived on device
- Opened - User tapped notification
- Clicked - User tapped action button
Location Tracking
Enable location-based messaging capabilities.
Initialize Location Tracking
final config = ZixflowConfig(
apiKey: 'YOUR_API_KEY',
locationConfig: LocationConfig(), // Enable location tracking
);
await Zixflow.initialize(config: config);
Request Location Permission
Use the permission_handler package:
dependencies:
permission_handler: ^11.3.1
import 'package:permission_handler/permission_handler.dart';
Future<void> requestLocationPermission() async {
final status = await Permission.location.request();
if (status.isGranted) {
print('Location permission granted');
} else if (status.isDenied) {
print('Location permission denied');
} else if (status.isPermanentlyDenied) {
// Open app settings
openAppSettings();
}
}
Manual Location Update
// Track location manually
Zixflow.location.trackLocation(
latitude: 37.7749,
longitude: -122.4194,
);
Advanced Configuration
Full Configuration Options
final config = ZixflowConfig(
// Required
apiKey: 'YOUR_API_KEY',
// Logging level
logLevel: LogLevel.debug, // error, info, debug
// Auto-tracking options
autoTrackDeviceAttributes: true,
trackApplicationLifecycleEvents: true,
// Screen tracking
screenViewUse: ScreenView.all, // all, inApp, none
// Event batching
flushAt: 20, // Send events after 20 tracked
flushInterval: 30, // Or every 30 seconds
// Custom endpoints (advanced)
apiHost: 'custom-api.example.com',
cdnHost: 'custom-cdn.example.com',
// In-app messaging
inAppConfig: InAppConfig(),
// Push notifications
pushConfig: PushConfig(
// Push configuration options
),
// Location tracking
locationConfig: LocationConfig(),
);
await Zixflow.initialize(config: config);
Log Levels
Control SDK logging verbosity:
logLevel: LogLevel.error, // Errors only
logLevel: LogLevel.info, // Errors + informational
logLevel: LogLevel.debug, // All messages (development)
Screen Tracking Options
screenViewUse: ScreenView.all, // Track all screens
screenViewUse: ScreenView.inApp, // Track only in-app screens
screenViewUse: ScreenView.none, // Disable auto-tracking
Platform-Specific Setup
iOS Setup
1. Add GoogleService-Info.plist
- Download
GoogleService-Info.plistfrom Firebase Console - Add to
ios/Runner/directory in Xcode - Ensure it's added to target
2. Update Info.plist
Add required permissions to ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to send relevant notifications</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location to send relevant notifications</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
3. CocoaPods (Default)
Run pod install:
cd ios
pod install
4. Swift Package Manager (Optional)
To use SPM instead of CocoaPods, update pubspec.yaml:
dependencies:
zixflow:
version: ^1.1.5
native_sdk:
ios:
enable-swift-package-manager: true
Then run:
cd ios
pod install
Android Setup
1. Add google-services.json
- Download
google-services.jsonfrom Firebase Console - Place in
android/app/directory
2. Add Google Services Plugin
Update android/build.gradle:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
Update android/app/build.gradle:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.gms.google-services' // Add this
}
android {
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
}
}
3. Permissions
Add permissions to android/app/src/main/AndroidManifest.xml:
<manifest>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>
4. Enable Location (Optional)
To enable location tracking, add to android/gradle.properties:
zixflow_location_enabled=true
Example Apps
The SDK includes complete example applications:
Flutter Sample (SPM)
Location: /apps/flutter_sample_spm/
Demonstrates:
- SDK initialization
- Firebase Cloud Messaging
- In-app messaging with inbox
- Location tracking
- Event tracking and screen views
- User identification
- Settings configuration UI
Flutter Sample (CocoaPods)
Location: /apps/flutter_sample_cocoapods/
Demonstrates:
- Same features as SPM sample
- Uses CocoaPods for iOS dependencies
Running Example Apps
- Clone the repository
- Navigate to sample app:
cd apps/flutter_sample_spm - Create
.envfile with credentials:API_KEY=your_api_key - Get dependencies:
flutter pub get - Run the app:
flutter run
API Reference
Zixflow Core
// Initialize SDK
await Zixflow.initialize(config: ZixflowConfig);
// Get instance
Zixflow.instance
// Identify user
Zixflow.instance.identify({
required String userId,
Map<String, dynamic>? traits,
});
// Track event
Zixflow.instance.track({
required String name,
Map<String, dynamic>? properties,
int? timestamp,
});
// Track screen
Zixflow.instance.screen({
required String title,
Map<String, dynamic>? properties,
});
// Set profile attributes
Zixflow.instance.setProfileAttributes({
required Map<String, dynamic> attributes,
});
// Set device attributes
Zixflow.instance.setDeviceAttributes({
required Map<String, dynamic> attributes,
});
// Clear user identification
Zixflow.instance.clearIdentify();
// Register device token
Zixflow.instance.registerDeviceToken({
required String deviceToken,
});
ZixflowConfig
ZixflowConfig({
required String apiKey,
LogLevel? logLevel,
bool? autoTrackDeviceAttributes,
bool? trackApplicationLifecycleEvents,
ScreenView? screenViewUse,
String? apiHost,
String? cdnHost,
int? flushAt,
int? flushInterval,
InAppConfig? inAppConfig,
PushConfig? pushConfig,
LocationConfig? locationConfig,
});
In-App Messaging
// Subscribe to events
Zixflow.inAppMessaging.subscribeToEventsListener(handler);
// Inbox management
final inbox = Zixflow.inAppMessaging.inbox;
await inbox.getMessages();
inbox.messages().listen((messages) { /* ... */ });
inbox.markMessageOpened(message);
inbox.markMessageUnopened(message);
inbox.deleteMessage(message);
inbox.openMessage(message);
Location
// Track location
Zixflow.location.trackLocation({
required double latitude,
required double longitude,
});
Libraries
- config/in_app_config
- config/location_config
- config/push_config
- config/zixflow_config
- data_pipelines/_native_constants
- data_pipelines/zixflow_method_channel
- data_pipelines/zixflow_platform_interface
- extensions/map_extensions
- extensions/method_channel_extensions
- location/_native_constants
- location/method_channel
- location/platform_interface
- messaging_in_app
- messaging_in_app/_native_constants
- messaging_in_app/inbox_message
- messaging_in_app/inline_in_app_message_view
- messaging_in_app/method_channel
- messaging_in_app/notification_inbox
- messaging_in_app/platform_interface
- messaging_push/_native_constants
- messaging_push/method_channel
- messaging_push/platform_interface
- zixflow
- zixflow_config
- zixflow_enums
- zixflow_inapp
- zixflow_plugin_version
- zixflow_sdk
- zixflow_widgets
- Zixflow Flutter SDK widgets library