Skip to main content

React Native SDK Integration

React Native wrapper that bridges the native iOS and Android SDKs, giving you full platform capabilities with a single TypeScript API.

iOS 14.0 minimum

The native SDK requires iOS 14.0, while React Native 0.75 still defaults to 13.4. CocoaPods rejects the mismatch with a misleading error - "Specs satisfying the dependency were found, but they required a higher minimum deployment target" - which reads like a version-resolution problem rather than a platform one.

Raise it in your ios/Podfile:

platform :ios, [min_ios_version_supported, '14.0'].max

Taking the higher of the two keeps working when a future React Native raises its own floor past ours.

Features

  • Native Bridge - Wraps native iOS (Swift) and Android (Kotlin) SDKs
  • Event Tracking - Track custom events and screen views
  • User Identity - Identify, alias, and manage user attributes
  • Push Notifications - Register tokens, track clicks (FCM + APNs)
  • In-App Messaging - native and HTML messages you render yourself
  • E-Commerce - Track purchases, cart events, checkout
  • Offline Support - Events queued locally, synced when online
  • Auto-Retry - Exponential backoff on network failures

Requirements

  • React Native 0.72+
  • iOS 14.0+ / Android SDK 24+
  • TypeScript 5.0+ (recommended)

Installation

npm install @joryio/react-native-sdk

iOS Setup

cd ios && pod install

Android Setup

Add the Joryio package to your MainApplication.kt:

import io.joryio.reactnative.JoryioPackage

override fun getPackages() = PackageList(this).packages.apply {
add(JoryioPackage())
}

Quick Start

import Joryio from '@joryio/react-native-sdk';

// Initialize once in App.tsx
await Joryio.initialize(
'jry_sdk_ios_your_key', // Your SDK key
'api-eu1.joryio.com', // API host (bare hostname, no scheme)
{
enableDebug: __DEV__,
trackSessionStart: true,
}
);

Event Tracking

Track Custom Events

// Basic event
Joryio.track('Button Clicked');

// Event with properties
Joryio.track('Product Added', {
productId: 'SKU-123',
productName: 'Blue T-Shirt',
price: 29.99,
currency: 'USD',
});

// E-commerce events
Joryio.track('Checkout Started', {
value: 89.97,
items: [
{ productId: 'SKU-123', quantity: 2, price: 29.99 },
{ productId: 'SKU-456', quantity: 1, price: 29.99 },
],
});

Joryio.track('Order Completed', {
order_id: 'ORD-789',
value: 89.97,
currency: 'USD',
});

Track Screen Views

// In your screen components
Joryio.trackScreen('ProductDetail', { productId: 'SKU-123' });
Joryio.trackScreen('Cart');
Joryio.trackScreen('Checkout');

React Navigation Integration

import { NavigationContainer } from '@react-navigation/native';

function App() {
const routeNameRef = useRef<string>();

return (
<NavigationContainer
onStateChange={() => {
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName && currentRouteName !== routeNameRef.current) {
Joryio.trackScreen(currentRouteName);
routeNameRef.current = currentRouteName;
}
}}
>
{/* ... */}
</NavigationContainer>
);
}

User Identity

Identify Users

Call identify after login or when you know who the user is:

// After login
Joryio.identify('user-123');

// With attributes
Joryio.identify('user-123');
Joryio.setAttributes({
email: 'john@example.com',
firstName: 'John',
plan: 'premium',
});

Alias Users

Link anonymous activity to a known user (e.g., after signup):

Joryio.alias('user-123');

Reset (Logout)

Clear user identity and start a new anonymous session:

Joryio.reset();

User Attributes

// Set multiple attributes
Joryio.setAttributes({
firstName: 'John',
lastName: 'Doe',
plan: 'premium',
age: 28,
isVIP: true,
});

// Set a single attribute
Joryio.setAttribute('favoriteColor', 'blue');

// Increment a numeric attribute
Joryio.incrementAttribute('loginCount', 1);
Joryio.incrementAttribute('totalSpent', 29.99);

// Remove an attribute
Joryio.unsetAttribute('temporaryFlag');

Push Notifications

Setup with Firebase (React Native Firebase)

import messaging from '@react-native-firebase/messaging';

// Request permission
const authStatus = await messaging().requestPermission();

// Get and register token
const token = await messaging().getToken();
Joryio.registerPushToken(token);

// Listen for token refresh
messaging().onTokenRefresh((newToken) => {
Joryio.registerPushToken(newToken);
});

Handle Push Notification Clicks

import messaging from '@react-native-firebase/messaging';

// When app is in background and notification is tapped
messaging().onNotificationOpenedApp((remoteMessage) => {
const trackingId = remoteMessage.data?.joryio_tracking_id;
if (trackingId) {
Joryio.trackPushClick(trackingId);
}
});

// When app was killed and opened via notification
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage?.data?.joryio_tracking_id) {
Joryio.trackPushClick(remoteMessage.data.joryio_tracking_id);
}
});

Check Push Status

const enabled = await Joryio.isPushEnabled();
console.log('Push enabled:', enabled);

In-App Messaging

Messages display on their own. Install the SDK, send a campaign, and it appears - drawn by the native views, using your app's fonts, colours and dark mode. There is nothing to wire up.

You only need the rest of this section if you want to render messages in React instead - see Taking over rendering.

Delivery tokens

When the backend serves an eligible campaign it also issues a short-lived signed delivery token for it. The SDK echoes that token back when it reports an impression, click or dismissal, and the server verifies the signature before recording anything.

You do not have to do anything - the SDK handles this for you. It is documented because it changes what happens to a client that does not send one:

POST /v1/in-app/track   (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }

The token is what makes an impression trustworthy: without it, anyone holding the SDK key - which ships inside every app and page - could report impressions and clicks for a campaign that was never shown, and your reporting would count them.

If in-app impressions stop being recorded, check that the app is built against a current version of the SDK: a build made before delivery tokens existed sends no token, and the server will reject its impressions.

Two kinds of message content

Every message carries a kind that tells you which shape it is. Switch on it:

kindWhat it carriesHow to render it
'native'title, body, imageUrl, buttonsReact Native components - <Text>, <Image>, <Pressable>
'html'html, cssA WebView

Taking over rendering

Subscribing with onInAppMessage stops the SDK drawing and hands each message to your code instead, so you can render it with React Native components. You will not get two copies.

Do this when you want in-app messages to match the rest of your UI, or when your app must not link a web view - in which case also exclude the UI artifact from your build (joryio-android-ui on Android, the JoryioUI product on iOS).

Listen for Messages

import { useEffect, useState } from 'react';
import Joryio, { type InAppMessage } from '@joryio/react-native';

function App() {
const [message, setMessage] = useState<InAppMessage | null>(null);

useEffect(() => Joryio.onInAppMessage(setMessage), []);

return <>{message && <InAppMessageHost message={message} />}</>;
}

Render a native message

Native content is text, not markup. Put it in a <Text> - passing it to a WebView or dangerouslySetInnerHTML would reintroduce exactly the injection risk native avoids.

function InAppMessageHost({ message }: { message: InAppMessage }) {
if (message.kind === 'native') {
return (
<View>
{message.imageUrl && <Image source={{ uri: message.imageUrl }} />}
{message.title && <Text style={styles.title}>{message.title}</Text>}
<Text>{message.body}</Text>

{message.buttons.map((button) => (
<Pressable
key={button.id}
onPress={() => {
if (button.action === 'url' && button.url) Linking.openURL(button.url);
Joryio.trackInAppImpression(message.id, 'clicked');
}}
>
<Text>{button.text}</Text>
</Pressable>
))}
</View>
);
}

return <WebView source={{ html: `<style>${message.css}</style>${message.html}` }} />;
}

Apply the campaign's style

message.style carries the overrides the campaign author set. Every field is optional, and an absent one means inherit your app's own look - so apply only what is present rather than substituting defaults of your own:

const st = message.style;
const text = {
// 'auto' aligns by the MESSAGE's language, not the device locale -
// I18nManager.isRTL is the wrong test when one workspace sends Hebrew and
// English to the same app.
writingDirection: 'auto' as const,
textAlign: st?.textAlign === 'center' ? 'center' : 'auto',
...(st?.fontFamily ? { fontFamily: st.fontFamily } : {}),
};

<View style={[styles.card,
st?.backgroundColor ? { backgroundColor: st.backgroundColor } : null,
st?.cornerRadius != null ? { borderRadius: st.cornerRadius } : null]}>
<Text style={[styles.title, text,
st?.fontSize != null ? { fontSize: st.fontSize * 1.3 } : null]}>{message.title}</Text>
<Text style={[styles.body, text,
st?.fontSize != null ? { fontSize: st.fontSize } : null]}>{message.body}</Text>
</View>

Apply fontFamily only if your app bundles that font; keeping your own typeface is better than substituting an arbitrary one. If the campaign sets a button colour but no label colour, pick black or white by contrast against the fill - defaulting to white makes a pale brand colour invisible.

Allowing HTML messages

HTML messages are turned off by default. An HTML message runs author-supplied JavaScript inside your app, so turning it on is a decision your app team makes - not something switched on from a marketing dashboard:

await Joryio.initialize('jry_sdk_YOUR_KEY', 'api-eu1.joryio.com', {
allowHtmlJsInAppMessages: true, // default: false
});

Leaving it off does not disable in-app messaging - native messages still arrive. Only HTML campaigns are skipped.

Track Impressions

// When message is displayed
Joryio.trackInAppImpression(message.id, 'displayed');

// When user clicks
Joryio.trackInAppImpression(message.id, 'clicked');

// When user dismisses
Joryio.trackInAppImpression(message.id, 'dismissed');

Configuration Options

OptionTypeDefaultDescription
enableDebugbooleanfalseEnable debug logging
logLevelstring'info'Log level: debug, info, warn, error
batchSizenumber50Events per batch before auto-flush (native default)
flushIntervalnumber5000Auto-flush interval in ms (native default)
sessionTimeoutnumber1800000Session timeout in ms (native default: 30 min)
trackSessionStartbooleantrueAuto-track session start events
userIdstringnullPre-set user ID at init time

Utilities

// Flush events immediately (before app close, logout, etc.)
Joryio.flush();

// Get IDs
const anonymousId = await Joryio.getAnonymousId();
const userId = await Joryio.getUserId(); // null if not identified
const sessionId = await Joryio.getSessionId();

TypeScript Support

The SDK is fully typed. Import types as needed:

import Joryio, {
type JoryioConfig,
type UserAttributes,
type InAppMessage,
} from '@joryio/react-native-sdk';