klaviyo-expo-plugin
v1.1.0
Published
Expo plugin for Klaviyo integration
Readme
klaviyo-expo-plugin
Contents
Introduction
The Klaviyo Expo plugin is a configuration plugin for Expo that automates the integration of Klaviyo's native SDKs into your Expo project. This plugin handles all the necessary native code modifications required for Klaviyo functionality, eliminating the need for manual native code changes.
The plugin is designed to work with the klaviyo-react-native-sdk and automates the setup of:
- Push notification open tracking
- Rich push notification support
- Badge count management (iOS)
- Key:value pair data reading
- Notification service extension setup (iOS)
- Icon / color notification configuration (Android)
- Universal links / App links support
Upgrading from an earlier version? See the Migration Guide for breaking changes and the steps to move between versions.
Requirements
Expo
- Your Expo app needs to be run as a development build. This plugin will not work in Expo Go.
- Supported: Expo SDK 54 through 57. That matches Expo's own documented support window. SDK 57 is the primary target.
- SDK 52 and 53 are covered by CI and did build and launch in testing, but are not a support commitment. Expo no longer documents them.
- The
expopeer range is*, following Expo's guidance for library authors. npm will therefore not block installation on any SDK. The table below is the support statement, not the peer range. On an older unsupported SDK the plugin prints a warning duringexpo prebuild. There is no upper ceiling, so a newer SDK never warns.
| Expo SDK | Status | Evidence |
| --- | --- | --- |
| 49.x and older | Not supported | Builds, but push silently never works. See below |
| 50.x | Not supported | Android build impossible. See below |
| 51.x | Not supported | Android build impossible. See below |
| 52.0.x | Tested, not supported | Compiled and launched, iOS + Android |
| 53.0.x | Tested, not supported | Compiled and launched, iOS + Android |
| 54.0.x | Supported | Compiled and launched, iOS + Android |
| 55.0.x | Supported | Compiled and launched, iOS + Android |
| 56.0.x | Supported | Compiled and launched, iOS + Android |
| 57.0.x | Supported (primary target) | Compiled and launched, iOS + Android |
Why older SDKs are unsupported. SDK 50 and 51:
klaviyo-react-native-sdkpullsklaviyo-android-sdk, which strictly requiresandroidx.core1.16.0. That needs compileSdk 35 and Android Gradle Plugin 8.6, while SDK 50 and 51 ship compileSdk 34 and AGP 8.1 to 8.2. No Android build is possible on those SDKs, and no plugin setting avoids it. SDK 49 and older: Expo autolinking in those versions does not understand theappleplatform key this plugin uses, so its native iOS module is dropped without any error. Prebuild succeeds and the app builds, but push notification handling is never installed. Use SDK 54 or newer.
Note on Expo 56 and iOS deployment targets.
expo-modules-coreraised its iOS deployment target to 16.4 in 56.0.0. Thereconcile_expo_module_deployment_targetsstep that lifts this plugin's pod to match arrived inexpo-modules-autolinking56.0.13. Every[email protected]release depends on that package with a range that resolves to the newest 56.0.x, so a normal install already has the fix. If your lockfile pinsexpo-modules-autolinkingat 56.0.12 or lower, runnpm update expo-modules-autolinking.
Android
minSdkVersionof24+compileSdkVersionof35+
The compileSdk floor is 35, not 36: this plugin sets no compileSdk itself, and the binding
constraint is androidx.core 1.16.0 (pulled in by klaviyo-android-sdk via
klaviyo-react-native-sdk), which requires 35. That is also why Expo SDK 50 and 51 cannot work -
they ship 34. Expo SDK 57 defaults to 36, so a normal install is already above the floor; 35 is
stated here so a working project on 35 is not told it is unsupported.
iOS
- Minimum Deployment Target
15.1+(the floor declared by the plugin's pod). Expo SDK 56 and later require an app deployment target of16.4 - Apple Push Notification Service (APNs) set up
⚠️ Important Note for Federated Apple Developer Accounts: If you're using a federated Apple Developer account, you'll need to provide an ASC API token with Admin access. While Expo supports federated accounts through ASC API tokens, the EAS CLI cannot directly log into federated accounts for credential management. See Expo's documentation on federated accounts for more details.
Installation
- Install the plugin in your Expo project. You need both
klaviyo-react-native-sdkandklaviyo-expo-pluginfor full functionality:
npm install klaviyo-react-native-sdk
npx expo install klaviyo-expo-plugin- Add the plugin to your
app.jsonorapp.config.js:
{
"expo": {
"plugins": [
//... other plugins
[
"klaviyo-expo-plugin",
{
"android": {
"logLevel": 1,
"automaticPushOpenTracking": true,
"notificationIconFilePath": "./your/notification/icon/path.png",
"notificationColor": "#00FF00"
},
"ios": {
"badgeAutoclearing": true,
"codeSigningStyle": "Automatic",
"devTeam": undefined // your devTeam ID here
}
}
]
]
}
}- Run a
prebuildto apply the Expo plugin to your project:
npx expo prebuild⚠️
prebuildchanged in Expo SDK 57. From SDK 57,npx expo prebuilderases theios/andandroid/folders and generates them again on every run. It preserves nothing. Not gitignored files, notPods/, not your.xcworkspace. If you keep anything in those folders that the plugin does not generate, use--no-cleanfor the previous additive behavior:npx expo prebuild --no-clean
- (optional) We recommend using the
expo-notificationslibrary for push permissions, token retrieval, and reading push content. Check out our/exampleproject for some ideas on how to use this.
Configuration
Plugin props
| Plugin prop | Type | Required | Description |
|-------------|------|----|-------------|
| android.logLevel | int | optional | Sets the logging level for the Klaviyo Android SDK. Default: 1 (DEBUG). Values: 0 (NONE), 1 (VERBOSE), 2 (DEBUG), 3 (INFO), 4 (WARNING), 5 (ERROR), 6 (ASSERT) |
| android.notificationIconFilePath | string | optional | Path to the notification icon file. Should be a white, transparent PNG. Default: none specified. Note that you should set this instead of expo-notifications, as they can conflict with each other. |
| android.notificationColor | string | optional | Hex color for notification accent. Must be a valid hex value, e.g., "#FF0000" Default: undefined |
| android.geofencingEnabled | boolean | optional | Controls whether the full location module (with geofencing and permissions) is included. When false, only the lightweight location-core module is included (no location permissions). Sets the klaviyoIncludeLocation gradle property. Default: false |
| android.formsEnabled | boolean | optional | Controls whether the full forms module (in-app forms rendering with WebView) is included. When false, only the lightweight forms-core module is included. Sets the klaviyoIncludeForms gradle property. Default: true |
| android.automaticPushOpenTracking | boolean | optional | Tracks notification opens automatically. Set to false to opt out and call Klaviyo.handlePush(intent) yourself. Default: true |
| android.automaticPushTokenForwarding | boolean | optional | Forwards the FCM push token to Klaviyo. Leaving this unset keeps the native SDK's default (forwards a token only when FCM delivers one); set to true to additionally fetch the token at init/foreground, or false to disable forwarding entirely. Default: unset |
| ios.badgeAutoclearing | boolean | optional | Enables automatic badge count clearing when app is opened. Default: true |
|ios.codeSigningStyle| string | optional | Declares management style for Code Signing Identity, Entitlements, and Provisioning Profile handled through XCode. Must be either "Manual" or "Automatic". Default: "Automatic". Note: We highly recommend using the automatic signing style. If you select manual, you may need to go into your developer.apple.com console and import the appropriate files and enable capabilities yourself.|
|ios.devTeam| string | optional| The 10-digit alphanumeric Apple Development Team ID associated with the necessary signing capabilities, provisioning profile, etc. Format: "XXXXXXXXXX" Default: undefined|
|ios.geofencingEnabled| boolean | optional | Enables geofencing/location tracking support. When true, injects the necessary dependencies to set up registering for geofencing on app launch. When false, sets the KLAVIYO_INCLUDE_LOCATION Podfile ENV var to exclude the KlaviyoLocation pod. See Geofencing below. Default: false (geofencing disabled)|
|ios.formsEnabled| boolean | optional | Controls whether the full forms module (in-app forms rendering with WebView) is included on iOS. When false, sets the KLAVIYO_INCLUDE_FORMS Podfile ENV var to exclude the module. Default: true|
|ios.includeNotificationServiceExtension| boolean | optional | Controls whether the Notification Service Extension (NSE) target is automatically set up. Set to false if you already have an NSE (e.g., from another SDK) to prevent conflicts — iOS only executes one NSE per app. Default: true|
|ios.automaticPushTokenForwarding| boolean | optional | Forwards the APNs push token to Klaviyo automatically. Opt-in, because on iOS this relies on app-delegate swizzling. When false, call Klaviyo.setPushToken(...) yourself. Default: false |
⚠️ Important: The two
android.automaticPush*props requireklaviyo-react-native-sdkversion 2.5.0 or higher, which bundles the native Android SDK that reads them. On older versions the generated manifest flags are ignored: opens are not tracked (theMainActivitycode generation that used to do this was removed in v1.0.0), and setting either prop tofalsedoes not opt you out. Make sure you have the correct version installed:npm install klaviyo-react-native-sdk@^2.5.0
Note: unlike automaticPushOpenTracking, this plugin does not inject a default value for
automaticPushTokenForwarding — omitting it leaves the manifest flag unset, which forwards a token
reactively (whenever FCM delivers one) but does not fetch proactively at startup. Set this prop to
true if you want the SDK to also fetch the token at initialize/foreground.
iOS push opens are tracked automatically and need no configuration, which is why there is no
ios.automaticPushOpenTracking prop.
Note: If you do not need to specify any of these for your project, it will use the defaults defined here. If you do not specify any of these props, you can add the plugin without additional arguments:
{
"expo": {
"plugins": [
//... other plugins
"klaviyo-expo-plugin"
]
}
}Debug mode
If you'd like to see debug logs of your prebuild, add the following to your build command:
EXPO_DEBUG=true npx expo prebuild
EXPO_DEBUG=true npx expo run:platformPlease attach these to any build issues you have to help the team debug.
Required config values
In your configuration file, set the following:
| Property | Details |
|----------|---------|
| version | Your app version. Used by the plugin for CFBundleShortVersionString and MARKETING_VERSION on iOS. Format: "X.X.X" (e.g. "1.0" or "2.3.1"). Default: "1.0" if not set. |
| ios.buildNumber | Build number for your iOS app. Used by the plugin for CFBundleVersion and CURRENT_PROJECT_VERSION. Format: "42". Default: "1" if not set. |
| ios.bundleIdentifier | Bundle identifier for your iOS app. Format: "com.companyname.appname" |
| ios.infoPlist.UIBackgroundModes | set this to ["remote-notification"] to ensure you can receive background push notifications |
| android.package | Package name for your Android app. Format: "com.companyname.appname" |
These values are used in various native configuration files and must be properly set for the plugin to work correctly.
In addition, this plugin adds a KlaviyoNotificationServiceExtension target to support Notification Service Extension capabilities on iOS and utilizes app groups to sync and share data across the Klaviyo SDK modules. To support this added target and app group in your Expo configuration file, add:
"appExtensions": [
{
"targetName": "KlaviyoNotificationServiceExtension",
"bundleIdentifier": "{YOUR_BUNDLE_ID}.KlaviyoNotificationServiceExtension",
"entitlements": {
"com.apple.security.application-groups": ["group.{YOUR_BUNDLE_ID}.KlaviyoNotificationServiceExtension.shared"]
}
}
]to the extra.eas.build.experimental.ios body of your app config where {YOUR_BUNDLE_ID} is your own app's bundle id. (See our example app config.)
Universal Links
Universal Links (iOS) and App Links (Android) allow you to navigate to a particular page within your app when users tap on links from outside your app, such as from emails, websites, or push notifications. Klaviyo uses universal links for click tracking in email campaigns, allowing you to track user engagement while seamlessly directing them to your app.
The Klaviyo SDK provides the handleUniversalTrackingLink() method to automatically handle click tracking for Klaviyo universal links and redirect users to the intended destination within your app.
⚠️ Important: Universal link tracking requires
klaviyo-react-native-sdkversion 2.1.0 or higher. Make sure you have the correct version installed:npm install klaviyo-react-native-sdk@^2.1.0
iOS Setup
To enable universal links on iOS, you need to configure Associated Domains in your app configuration:
- Add the
associatedDomainsarray to youriosconfiguration inapp.json:
{
"expo": {
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp",
"associatedDomains": [
"applinks:trk.your-domain.com"
]
}
}
}- Verify domain ownership by hosting an
apple-app-site-associationfile on your domain. This file must be accessible athttps://trk.your-domain.com/.well-known/apple-app-site-associationorhttps://trk.your-domain.com/apple-app-site-association.
For more details on setting up Associated Domains, see Apple's documentation.
Android Setup
To enable App Links on Android, you need to configure intent filters in your app configuration:
- Add intent filters to your
androidconfiguration inapp.json:
{
"expo": {
"android": {
"package": "com.yourcompany.yourapp",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "trk.your-domain.com",
"pathPrefix": "/u"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}- Verify domain ownership by hosting a Digital Asset Links JSON file on your domain. This file must be accessible at
https://trk.your-domain.com/.well-known/assetlinks.json.
For more details on setting up Android App Links, see Android's documentation.
React Native Code
In your React Native code, use Expo's expo-linking library to handle both universal links and custom deep links:
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
import { Klaviyo } from 'klaviyo-react-native-sdk';
export default function App() {
useEffect(() => {
const handleUrl = (url: string) => {
// Check if this is a Klaviyo universal tracking link
if (Klaviyo.handleUniversalTrackingLink(url)) {
// Klaviyo SDK is handling the tracking link
// It will track the click and redirect to the destination URL
console.log('Klaviyo tracking link processed:', url);
return;
}
// Handle other deep links in your app (both custom schemes and universal links)
// This will be called after Klaviyo redirects from the tracking link to your destination
console.log('Navigating to destination:', url);
// Add your navigation logic here (e.g., using React Navigation)
};
// Handle the initial URL if the app was opened with a link
Linking.getInitialURL().then((url) => {
if (url) {
handleUrl(url);
}
});
// Listen for deep link events while the app is running
const subscription = Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
return () => {
subscription.remove();
};
}, []);
// Rest of your app code
return (
// Your app components
);
}How it works:
- When a user taps a Klaviyo tracking link (format:
https://trk.your-domain.com/u/...), your app opens - The
handleUrlfunction receives the URL Klaviyo.handleUniversalTrackingLink()checks if it's a Klaviyo tracking link:- If true: The SDK tracks the click event and resolves the tracking link to its destination URL, which triggers another call to
handleUrlwith the destination - If false: The URL is not a Klaviyo tracking link, so you should handle it as a regular deep link
- If true: The SDK tracks the click event and resolves the tracking link to its destination URL, which triggers another call to
- Your app navigates to the appropriate screen based on the destination URL
For more information on deep linking with Expo, see Expo's Linking documentation.
Geofencing
Geofencing enables location-based marketing by allowing Klaviyo to trigger events when users enter or exit specific geographic regions.
Configuration
To enable geofencing support, add the geofencingEnabled property to your plugin props:
{
"expo": {
"plugins": [
[
"klaviyo-expo-plugin",
{
"android": {
"geofencingEnabled": true
},
"ios": {
"geofencingEnabled": true
}
}
]
]
}
}| Property | Type | Required | Description |
|----------|------|----------|-------------|
| ios.geofencingEnabled | boolean | optional | Enables geofencing support on iOS. When true, injects the necessary dependencies to set up registering for geofencing on app launch. Default: false |
| android.geofencingEnabled | boolean | optional | Enables geofencing support on Android. When true, includes the full location module with geofencing and location permissions. When false, only the lightweight location-core module is included. Default: false |
When geofencing is enabled on iOS, the plugin will:
- Import
KlaviyoLocationand callKlaviyoSDK().registerGeofencing()in the app delegate - Add
locationtoUIBackgroundModesin Info.plist
When geofencing is disabled on iOS (default), the plugin sets the KLAVIYO_INCLUDE_LOCATION Podfile ENV var to 'false', which tells the RN SDK's podspec to exclude the KlaviyoLocation pod dependency.
When geofencing is enabled on Android, the plugin will:
- Set the
klaviyoIncludeLocationgradle property totrue, which includes the full location module with geofencing support and location permissions
Requesting Permissions
Users must grant location permissions at runtime. We recommend using expo-location (see docs here) to request permissions:
import * as Location from 'expo-location';
async function requestLocationPermissions() {
// First request foreground permission
const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync();
if (foregroundStatus !== 'granted') {
console.log('Foreground location permission denied');
return;
}
// Then request background permission for geofencing
const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync();
if (backgroundStatus !== 'granted') {
console.log('Background location permission denied');
return;
}
console.log('Location permissions granted for geofencing');
}See the example app for a complete implementation.
In-App Forms
By default, the full forms module is included on both platforms, which enables in-app form rendering via WebView. If your app does not use Klaviyo in-app forms, you can exclude the full module to reduce your app size:
{
"expo": {
"plugins": [
[
"klaviyo-expo-plugin",
{
"android": {
"formsEnabled": false
},
"ios": {
"formsEnabled": false
}
}
]
]
}
}When disabled on Android, only the lightweight forms-core module is included. On iOS, the KLAVIYO_INCLUDE_FORMS Podfile ENV var is set to 'false', which tells the RN SDK's podspec to exclude the KlaviyoForms pod dependency. The SDK will still function normally on both platforms, but any calls to forms APIs will no-op gracefully.
Example app
We created an example app to show how to use this plugin in coordination with the klaviyo-react-native-sdk. Set this up and run based on whichever platform you'd like to test on:
// from repo directory
cd example
npm run clean-android // for android
npm run clean-ios // for ios
npm run reset-all // deletes node modules and built foldersTroubleshooting
Common issues and solutions:
Push Notifications Not Working
- Verify FCM/APNs setup
- Check notification permissions
- Ensure proper configuration in app.json
- You may need to add the
aps-environmentto yourios.entitlementsin your app config if it is not there alreadyios: { entitlements: { 'aps-environment': 'development', // or 'production' // ... other entitlements } }
- You may need to add the
Deep Links / Universal Links Not Working
- Custom URL schemes: Verify URL scheme configuration in your app.json
- Universal Links (iOS):
- Verify
associatedDomainsis configured correctly in your app.json - Ensure your
apple-app-site-associationfile is properly hosted and accessible - Test the domain verification using Apple's app-site-association verification tool
- Make sure your domain supports HTTPS
- Verify
- App Links (Android):
- Check intent filters are configured with
autoVerify: truein your app.json - Ensure your
assetlinks.jsonfile is properly hosted athttps://your-domain.com/.well-known/assetlinks.json - Test the domain verification using the command:
adb shell pm get-app-links your.package.name - Verify the domain supports HTTPS
- Check intent filters are configured with
- Testing Universal Links:
- Universal links will NOT work when tapped from within the same app or in simulator under certain conditions
- Test by sending yourself an email with the link or using the Notes app
- On Android, use
adb shell am start -a android.intent.action.VIEW -d "https://your-tracking-link"to test
Build Errors
- Clean and rebuild the project
- Verify Expo SDK version compatibility
- Check native dependencies
EAS Errors
- (iOS) Check your Identifiers in the Apple Developer Console and ensure the main app target has Push Notifications and App Group capabilities checked, and your Notification Service Extension target has App Groups capability checked. Check that both App Group capabilities have the app group name with the appended
.KlaviyoNotificationServiceExtension.sharedsuffix - (iOS) You should have two different provisioning profiles generated for this project. One for the main target, one for the Notification Service Extension.
- (iOS) Ensure the correct provisioning profiles are being recognized by EAS, and declare the Notification Service Extension in the
extra.eas.build.experimental.ios.appExtensionsof your app config as mentioned here
- (iOS) Check your Identifiers in the Apple Developer Console and ensure the main app target has Push Notifications and App Group capabilities checked, and your Notification Service Extension target has App Groups capability checked. Check that both App Group capabilities have the app group name with the appended
License
The Klaviyo Expo Plugin is available under the terms of the MIT license.
