zixflow 1.1.5 copy "zixflow: ^1.1.5" to clipboard
zixflow: ^1.1.5 copied to clipboard

An official flutter plugin for Zixflow, an automated messaging platform.

Zixflow Flutter SDK - Integration Guide #

Complete guide for integrating the Zixflow Flutter SDK into your application.

Table of Contents #

  1. Introduction
  2. Requirements
  3. Installation
  4. Quick Start
  5. Core Features
  6. Push Notifications
  7. In-App Messaging
  8. Location Tracking
  9. Advanced Configuration
  10. Platform-Specific Setup
  11. Troubleshooting
  12. Example Apps
  13. 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.

pub.dev - Zixflow Flutter SDK

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:

  1. Log in to your Zixflow account
  2. Go to SettingsDevelopersAPI Keys
  3. 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:

  1. Configure FCM in Zixflow dashboard - Add FCM Server Key to Zixflow dashboard
  2. Firebase Project Setup - Create Firebase project and add both iOS and Android apps
  3. Device tokens must be associated with users - Call identify() before sending notifications
  4. 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

  1. Go to https://console.firebase.google.com
  2. Click Create a project or select existing project
  3. Follow the setup wizard

2. Add iOS App to Firebase

  1. In Firebase console, click Add app → Select iOS
  2. Enter iOS Bundle ID (e.g., com.yourcompany.app)
  3. Download GoogleService-Info.plist
  4. Add GoogleService-Info.plist to ios/Runner/ directory in Xcode

3. Add Android App to Firebase

  1. In Firebase console, click Add app → Select Android
  2. Enter Android package name (same as iOS bundle ID)
  3. Download google-services.json
  4. Place google-services.json in android/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

  1. Open <yourAppName>.xcworkspace in ios/ folder
  2. Select your main app target (Runner)
  3. Go to Signing & Capabilities tab
  4. Click + Capability → Add Push Notifications
  5. Click + Capability → Add Background Modes
  6. Enable Remote notifications under Background Modes

Step 2: Create Notification Service Extension

For rich push notifications (images, videos, GIFs):

1. Create Extension in Xcode:

  1. Select FileNewTarget
  2. Choose Notification Service Extension
  3. Name it NotificationServiceExtension
  4. Click Finish
  5. 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:

  1. Add FlutterGeneratedPluginSwiftPackage to NotificationServiceExtension target
  2. Go to GeneralFrameworks 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:

  1. Place white icon PNG in android/app/src/main/res/drawable/ic_notification.png
  2. 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

  1. Log into Zixflow dashboard
  2. Navigate to MessagingPush Notifications
  3. Create test push notification with image
  4. Send to specific user (using userId from identify())

Troubleshooting #

Firebase Configuration Issues

Error: GoogleService-Info.plist not found (iOS)

Solution:

  1. Ensure file is in ios/Runner/ directory
  2. Verify file is added to Xcode target (check target membership)
  3. Clean and rebuild: flutter clean && flutter pub get

Error: google-services.json not found (Android)

Solution:

  1. Ensure file is in android/app/ directory
  2. Verify Google Services plugin applied in android/app/build.gradle
  3. Sync Gradle files

Push Notifications Not Received

iOS Checklist:

  • ❌ Push Notifications capability enabled in Xcode
  • ❌ APNs certificate uploaded to Firebase
  • ❌ Testing on physical device (not simulator)
  • ❌ User identified: Zixflow.instance.identify()
  • ❌ Permission granted by user
  • ❌ Foreground display configured if app is active

Android Checklist:

  • google-services.json in android/app/ directory
  • ❌ Google Services plugin applied
  • ❌ FCM Server Key in Zixflow dashboard
  • ❌ POST_NOTIFICATIONS permission granted (Android 13+)
  • ❌ Device registered for push

Rich Push Not Working (Images)

iOS Solutions:

  1. Ensure Notification Service Extension properly configured
  2. Verify extension has correct dependencies in Podfile
  3. Check mutable-content: 1 in push payload
  4. Verify image URL is accessible
  5. Check extension logs for errors

Android Solutions:

  1. Verify image URL accessible from device
  2. Check internet connectivity
  3. Ensure image format supported (JPEG, PNG, GIF)

Permission Denied

Symptoms: User denied push permission

Solutions:

  1. Explain value before requesting permission
  2. Show in-app message about benefits
  3. 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:

  1. Verify Firebase initialized before calling getToken()
  2. Check Google Play Services available (Android)
  3. Ensure internet connectivity
  4. Review native logs for errors
  5. Rebuild app and retry

Best Practices #

  1. Initialize Firebase first - Before Zixflow SDK
  2. Request permission thoughtfully - Explain value before asking
  3. Identify users immediately - After authentication
  4. Test on physical devices - Simulators have limitations
  5. Use rich push - Images increase engagement
  6. Handle deep links - Provide seamless navigation
  7. Monitor metrics - Track delivery and open rates
  8. Enable debug logging - During development only
  9. Test both platforms - iOS and Android behave differently
  10. Clear identity on logout - Call clearIdentify()
  11. 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:

  1. Delivery Confirmed - When the notification arrives on the device
  2. Notification Opened - When the user taps the notification
  3. 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)
  • deeplink may 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

  1. Log into Zixflow dashboard
  2. Navigate to MessagingPush Notifications
  3. 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

  1. Download GoogleService-Info.plist from Firebase Console
  2. Add to ios/Runner/ directory in Xcode
  3. 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

  1. Download google-services.json from Firebase Console
  2. 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 #

  1. Clone the repository
  2. Navigate to sample app:
    cd apps/flutter_sample_spm
    
  3. Create .env file with credentials:
    API_KEY=your_api_key
    
  4. Get dependencies:
    flutter pub get
    
  5. 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,
});
0
likes
150
points
293
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An official flutter plugin for Zixflow, an automated messaging platform.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on zixflow

Packages that implement zixflow