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.
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:
kind | What it carries | How to render it |
|---|---|---|
'native' | title, body, imageUrl, buttons | React Native components - <Text>, <Image>, <Pressable> |
'html' | html, css | A 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
| Option | Type | Default | Description |
|---|---|---|---|
enableDebug | boolean | false | Enable debug logging |
logLevel | string | 'info' | Log level: debug, info, warn, error |
batchSize | number | 50 | Events per batch before auto-flush (native default) |
flushInterval | number | 5000 | Auto-flush interval in ms (native default) |
sessionTimeout | number | 1800000 | Session timeout in ms (native default: 30 min) |
trackSessionStart | boolean | true | Auto-track session start events |
userId | string | null | Pre-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';