Skip to content
 
 

Repository files navigation

“This project is based on code originally developed by Denisr under the BSD 3-Clause License.”

klaviyo_flutter

Pub CI

A comprehensive Flutter wrapper for Klaviyo's Android SDK and iOS SDK. Track events, manage user profiles, and handle push notifications with ease.

Features

  • Event Tracking: Log custom events with optional metadata
  • Profile Management: Create and update user profiles with identifiers and attributes
  • Push Notifications: Full support for Firebase Cloud Messaging (Android) and APNs (iOS)
  • Type Safety: Strongly-typed API with null safety support
  • Cross-Platform: Unified API for both iOS and Android

Requirements

  • Android: Minimum SDK version 23+, uses Klaviyo Android SDK v3.3.1
  • iOS: iOS 13.0+, uses Klaviyo Swift SDK v4.2.1
  • Dart: SDK 3.2.0 or higher
  • Flutter: 3.0.0 or higher
  • Firebase: This plugin requires firebase_messaging for push notifications on both platforms

Installation

Add klaviyo_flutter to your pubspec.yaml:

dependencies:
  klaviyo_flutter: ^0.3.0

Then run:

flutter pub get

Quick Start

import 'package:flutter/material.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize with your Klaviyo public API key
  await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');

  runApp(MyApp());
}

Get your public API key from Klaviyo Account Settings.

Usage Guide

1. Initialization

Initialize Klaviyo before using any other methods, typically in your main() function:

await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');

// Check if already initialized
if (Klaviyo.instance.isInitialized) {
  print('Klaviyo is ready!');
}

2. Event Tracking

Track user actions and behaviors in your app:

Basic Event

await Klaviyo.instance.logEvent('Viewed Product');

Event with Metadata

await Klaviyo.instance.logEvent(
  'Added to Cart',
  {
    'product_id': '12345',
    'product_name': 'Blue T-Shirt',
    'price': 29.99,
    'quantity': 2,
  },
);

E-commerce Events

Klaviyo supports special e-commerce events with the $ prefix:

// Track a purchase
await Klaviyo.instance.logEvent(
  '\$successful_payment',
  {
    '\$value': 149.99,
    'order_id': 'ORD-789',
    'items': ['item1', 'item2'],
  },
);

// Track product views
await Klaviyo.instance.logEvent(
  'Viewed Product',
  {
    '\$product_id': 'SKU-123',
    '\$product_name': 'Wireless Headphones',
    '\$value': 79.99,
  },
);

3. Profile Management

Update Profile with KlaviyoProfile (Recommended)

Use KlaviyoProfile for bulk updates with all fields optional:

await Klaviyo.instance.updateProfile(
  KlaviyoProfile(
    id: 'customer-42',                    // External ID
    email: 'user@example.com',
    phoneNumber: '+15555551212',
    firstName: 'Jane',
    lastName: 'Doe',
    organization: 'Acme Corp',
    title: 'Product Manager',
    image: 'https://example.com/avatar.jpg',
    address1: '123 Main St',
    address2: 'Apt 4B',
    city: 'San Francisco',
    country: 'United States',
    region: 'CA',
    zip: '94102',
    timezone: 'America/Los_Angeles',
    latitude: 37.7749,
    longitude: -122.4194,
    properties: {
      'app_version': '2.0.5',
      'premium_member': true,
      'loyalty_points': 1250,
    },
  ),
);

Individual Profile Methods

Set profile attributes one at a time:

// Set identifiers
await Klaviyo.instance.setExternalId('customer-123');
await Klaviyo.instance.setEmail('user@example.com');
await Klaviyo.instance.setPhoneNumber('+15555551212');

// Set name fields
await Klaviyo.instance.setFirstName('Jane');
await Klaviyo.instance.setLastName('Doe');

// Set organization info
await Klaviyo.instance.setOrganization('Acme Corp');
await Klaviyo.instance.setTitle('Product Manager');
await Klaviyo.instance.setImage('https://example.com/avatar.jpg');

// Set address fields
await Klaviyo.instance.setAddress1('123 Main St');
await Klaviyo.instance.setAddress2('Apt 4B');
await Klaviyo.instance.setCity('San Francisco');
await Klaviyo.instance.setCountry('United States');
await Klaviyo.instance.setRegion('CA');
await Klaviyo.instance.setZip('94102');
await Klaviyo.instance.setTimezone('America/Los_Angeles');

// Set location coordinates
await Klaviyo.instance.setLatitude(37.7749);
await Klaviyo.instance.setLongitude(-122.4194);

// Set custom attributes
await Klaviyo.instance.setCustomAttribute('favorite_color', 'blue');
await Klaviyo.instance.setCustomAttribute('membership_level', 'gold');

Get Profile Information

Retrieve current profile identifiers:

String? externalId = await Klaviyo.instance.getExternalId();
String? email = await Klaviyo.instance.getEmail();
String? phoneNumber = await Klaviyo.instance.getPhoneNumber();

print('Current user: $email (ID: $externalId)');

Reset Profile

Clear all profile data (e.g., on user logout):

await Klaviyo.instance.resetProfile();

Note: After resetting, you'll need to call sendTokenToKlaviyo() again to associate the device with a new profile.

4. Working with KlaviyoProfile

The KlaviyoProfile class provides convenient methods for profile manipulation:

Create from JSON

final profile = KlaviyoProfile.fromJson({
  'external_id': 'user-123',
  'email': 'user@example.com',
  'first_name': 'Jane',
  'latitude': 37.7749,
  'properties': {'app_version': '1.0.0'},
});

Copy with Changes

Create a modified copy while preserving other fields:

final updatedProfile = profile.copyWith(
  city: 'New York',
  zip: '10001',
  properties: {'app_version': '1.1.0'},
);

Convert to JSON

Map<String, dynamic> json = profile.toJson();

5. Push Notifications

Architecture

This plugin provides a unified push notification stream for both iOS and Android:

  • iOS: Push events are handled natively via UNUserNotificationCenterDelegate and streamed to Flutter
  • Android: The plugin automatically bridges firebase_messaging streams internally, transforming RemoteMessage objects into KlaviyoPushEvent

This means you only need to listen to Klaviyo.instance.onPushEvent - the plugin handles the platform differences for you.

Setup Overview

  1. Configure Firebase Cloud Messaging for both platforms
  2. Initialize Firebase in your app (required for both platforms)
  3. Initialize the Klaviyo push bridge
  4. Listen to unified push events (received/opened/dismissed/action)
  5. Handle deep links and analytics in your app

Firebase Prerequisites

  • Create a Firebase project and add your Android/iOS apps
  • Download google-services.json (Android) and GoogleService-Info.plist (iOS)
  • Enable Firebase Cloud Messaging
  • Copy the FCM server key to Klaviyo Dashboard (Account > Settings > Push)
  • Add firebase_core and firebase_messaging to your app's dependencies:
dependencies:
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0
  klaviyo_flutter: ^0.3.0

Android Configuration

  1. Add Google Services Plugin

    Place google-services.json in android/app/ and update android/app/build.gradle:

    dependencies {
        classpath 'com.google.gms:google-services:4.3.15'
    }
    
    // At the bottom of the file
    apply plugin: 'com.google.gms.google-services'
  2. Initialize Firebase in your app

    The plugin relies on firebase_messaging for Android push handling. Initialize Firebase before using Klaviyo:

    import 'package:firebase_core/firebase_core.dart';
    
    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp();
      // Then initialize Klaviyo...
    }
  3. Add Permissions

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  4. Optional: Custom Notification Icon

    <meta-data
        android:name="com.klaviyo.push.default_notification_icon"
        android:resource="@drawable/ic_notification" />
  5. Enable AndroidX

    In android/gradle.properties:

    android.useAndroidX=true
    android.enableJetifier=true

Note: On Android, push notifications are handled by firebase_messaging. The plugin automatically listens to Firebase Messaging streams and converts them to unified KlaviyoPushEvent objects. You do not need to set up your own Firebase Messaging listeners for push events - just use Klaviyo.instance.onPushEvent.

iOS Configuration

  1. Add GoogleService-Info.plist

    Drag the file into the Runner target in Xcode.

  2. Enable Capabilities

    In Xcode, enable:

    • Push Notifications
    • Background Modes > Remote notifications
  3. Update Info.plist

    <key>NSPhotoLibraryUsageDescription</key>
    <string>We need access to your photo library</string>
  4. Set Minimum Deployment Target

    In ios/Runner.xcodeproj/project.pbxproj:

    IPHONEOS_DEPLOYMENT_TARGET = 13.0;
    

Requesting Push Permissions

import 'package:firebase_messaging/firebase_messaging.dart';

// Request permission (iOS prompts user, Android auto-grants on API 33+)
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
  alert: true,
  badge: true,
  sound: true,
);

if (settings.authorizationStatus == AuthorizationStatus.authorized) {
  print('User granted permission');
  // Tokens are automatically registered with Klaviyo when received.
  // You can optionally get the token manually if needed:
  // String? token = await FirebaseMessaging.instance.getToken();
}

Flutter Usage

import 'package:firebase_core/firebase_core.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase first (required for Android push)
  await Firebase.initializeApp();

  // Initialize Klaviyo with your public API key
  // This also initializes the push notification bridge:
  // - On iOS: sets up native event channel
  // - On Android: automatically subscribes to Firebase Messaging streams
  await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');

  // Check if app was launched from a notification
  final initial = await Klaviyo.instance.getInitialNotification();
  if (initial != null) {
    // Handle deep links or analytics from a terminated launch
    print('Launched from notification: ${initial.title}');
  }

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _setupPushListeners();
  }

  void _setupPushListeners() {
    // Listen to all push events
    Klaviyo.instance.onPushEvent.listen((event) {
      switch (event.type) {
        case KlaviyoPushEventType.received:
          print('Push received: ${event.title}');
          break;
        case KlaviyoPushEventType.opened:
          print('Push opened: ${event.title}');
          // Handle deep link
          if (event.deepLink != null) {
            // Navigate to deep link
          }
          break;
        case KlaviyoPushEventType.dismissed:
          print('Push dismissed');
          break;
        case KlaviyoPushEventType.actionTapped:
          print('Action tapped: ${event.actionId}');
          break;
      }
    });

    // Or use specific streams
    Klaviyo.instance.onPushOpened.listen((event) {
      print('Push opened with appState: ${event.appState}');
    });

    // Listen for token refreshes (auto-registered with Klaviyo)
    Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
      print('Token refreshed: ${tokenEvent.token}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HomeScreen());
  }
}

Check if Push is from Klaviyo

// Check rawData for Klaviyo marker
if (event.rawData.containsKey('_k')) {
  // This is a Klaviyo push
}

Update Badge Count (iOS Only)

// Set badge count on app icon
await Klaviyo.instance.setBadgeCount(5);

// Clear badge
await Klaviyo.instance.setBadgeCount(0);

Note: setBadgeCount() only works on iOS. On Android, it's a no-op because badge behavior varies by manufacturer.

Listen for Token Refreshes

Tokens are automatically registered with Klaviyo when refreshed. You can also listen for token events in Flutter:

Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
  print('Token refreshed and auto-registered: ${tokenEvent.token}');
});

6. Complete Example

Here's a full example demonstrating all major features:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase first
  await Firebase.initializeApp();

  // Initialize Klaviyo (also sets up push notification bridge)
  await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');

  // Check for launch notification
  final initialPush = await Klaviyo.instance.getInitialNotification();

  runApp(MyApp(initialPush: initialPush));
}

class MyApp extends StatelessWidget {
  final KlaviyoPushEvent? initialPush;

  const MyApp({this.initialPush});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: KlaviyoDemo(initialPush: initialPush),
    );
  }
}

class KlaviyoDemo extends StatefulWidget {
  final KlaviyoPushEvent? initialPush;

  const KlaviyoDemo({this.initialPush});

  @override
  State<KlaviyoDemo> createState() => _KlaviyoDemoState();
}

class _KlaviyoDemoState extends State<KlaviyoDemo> {
  String _status = 'Ready';

  @override
  void initState() {
    super.initState();
    _initializeKlaviyo();
    _setupPushListeners();

    // Handle initial push if app was launched from notification
    if (widget.initialPush != null) {
      _handlePushEvent(widget.initialPush!);
    }
  }

  void _setupPushListeners() {
    // Listen for all push events
    Klaviyo.instance.onPushEvent.listen(_handlePushEvent);

    // Token refreshes are auto-registered with Klaviyo
    Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
      setState(() => _status = 'Token registered');
    });
  }

  void _handlePushEvent(KlaviyoPushEvent event) {
    setState(() => _status = 'Push ${event.type.name}: ${event.title ?? ""}');

    // Handle deep links
    if (event.type == KlaviyoPushEventType.opened && event.deepLink != null) {
      // Navigate to deep link
    }
  }

  Future<void> _identifyUser() async {
    try {
      await Klaviyo.instance.updateProfile(
        KlaviyoProfile(
          id: 'user-12345',
          email: 'demo@example.com',
          firstName: 'Demo',
          lastName: 'User',
          phoneNumber: '+15555551212',
          city: 'San Francisco',
          country: 'United States',
          properties: {
            'app_version': '1.0.0',
            'platform': 'flutter',
          },
        ),
      );
      setState(() => _status = 'User identified');
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
  }

  Future<void> _trackEvent() async {
    try {
      await Klaviyo.instance.logEvent(
        'Button Clicked',
        {
          'button_name': 'Track Event',
          'timestamp': DateTime.now().toIso8601String(),
        },
      );
      setState(() => _status = 'Event tracked');
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
  }

  Future<void> _trackPurchase() async {
    try {
      await Klaviyo.instance.logEvent(
        '\$successful_payment',
        {
          '\$value': 99.99,
          'order_id': 'ORD-${DateTime.now().millisecondsSinceEpoch}',
          'item_count': 3,
        },
      );
      setState(() => _status = 'Purchase tracked');
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
  }

  Future<void> _logout() async {
    try {
      await Klaviyo.instance.resetProfile();
      setState(() => _status = 'Logged out');
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Klaviyo Flutter Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Status: $_status',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 40),
            ElevatedButton(
              onPressed: _identifyUser,
              child: Text('Identify User'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _trackEvent,
              child: Text('Track Event'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _trackPurchase,
              child: Text('Track Purchase'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _logout,
              style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
              child: Text('Logout'),
            ),
          ],
        ),
      ),
    );
  }
}

API Reference

Klaviyo Class

Method Parameters Returns Description
initialize() String apiKey Future<void> Initialize the SDK with your public API key
logEvent() String name, [Map<String, dynamic>? metaData] Future<String> Log a custom event with optional metadata
updateProfile() KlaviyoProfile profile Future<String> Update profile with bulk attributes
setExternalId() String id Future<void> Set external identifier
getExternalId() - Future<String?> Get current external ID
setEmail() String email Future<void> Set email address
getEmail() - Future<String?> Get current email
setPhoneNumber() String phoneNumber Future<void> Set phone number
getPhoneNumber() - Future<String?> Get current phone number
setFirstName() String firstName Future<void> Set first name
setLastName() String lastName Future<void> Set last name
setOrganization() String organization Future<void> Set organization
setTitle() String title Future<void> Set job title
setImage() String image Future<void> Set profile image URL
setAddress1() String address Future<void> Set address line 1
setAddress2() String address Future<void> Set address line 2
setCity() String city Future<void> Set city
setCountry() String country Future<void> Set country
setRegion() String region Future<void> Set region/state
setZip() String zip Future<void> Set postal code
setTimezone() String timezone Future<void> Set timezone
setLatitude() double latitude Future<void> Set latitude coordinate
setLongitude() double longitude Future<void> Set longitude coordinate
setCustomAttribute() String key, String value Future<void> Set custom profile attribute
resetProfile() - Future<void> Clear all profile data
sendTokenToKlaviyo() String token Future<void> Register device token for push
handlePush() Map<String, dynamic> message Future<bool> Handle incoming push notification
isKlaviyoPush() Map<String, dynamic> message bool Check if push is from Klaviyo
setBadgeCount() int count Future<void> Set app badge count (iOS only)
isInitialized - bool Check if SDK is initialized

KlaviyoProfile Class

Property Type Description
id String? External user identifier
email String? Email address
phoneNumber String? Phone number (format guide)
firstName String? First name
lastName String? Last name
organization String? Company/organization name
title String? Job title
image String? Profile image URL
address1 String? Address line 1
address2 String? Address line 2
city String? City
country String? Country
region String? State/province/region
zip String? Postal/ZIP code
timezone String? Timezone (e.g., 'America/New_York')
latitude double? Latitude coordinate
longitude double? Longitude coordinate
properties Map<String, dynamic>? Custom properties

Known Limitations

Clearing Individual Profile Fields

This plugin does not support clearing individual profile fields (e.g., setting a field to null). This is due to the underlying Klaviyo API behavior where omitting a field leaves it unchanged, and null values are not processed.

To clear profile fields: Use Klaviyo.resetProfile() to clear all profile data, then call updateProfile() with only the fields you want to keep:

// Clear all profile data
await Klaviyo.instance.resetProfile();

// Set only the fields you want to keep
await Klaviyo.instance.updateProfile(
  KlaviyoProfile(
    email: 'user@example.com',
    firstName: 'Jane',
    // All other fields are now cleared
  ),
);

Troubleshooting

Android Issues

Build fails with "Duplicate class" errors

Enable Jetifier in android/gradle.properties:

android.enableJetifier=true

Push notifications not received

  • Verify google-services.json is in android/app/
  • Ensure Firebase is initialized before Klaviyo.instance.initialize()
  • Confirm FCM server key is added to Klaviyo dashboard
  • Check that firebase_messaging is properly configured in your app

iOS Issues

Build fails with deployment target error

Set minimum deployment target in ios/Runner.xcodeproj/project.pbxproj:

IPHONEOS_DEPLOYMENT_TARGET = 13.0;

Push notifications not received

  • Verify Push Notifications capability is enabled
  • Check that APNs certificates are configured in Apple Developer Portal
  • Ensure device token is being sent to Klaviyo

General Issues

"API key must not be empty" error

Make sure you're using your public API key, not the private key.

Events not appearing in Klaviyo

  • Check that initialize() was called before logging events
  • Verify API key is correct
  • Events may take a few minutes to appear in the dashboard

Resources

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

See the LICENSE file for details.

Changelog

See CHANGELOG.md for version history.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages