This article explains how to perform a full integration of a React Native app with Releva, using the Releva React Native SDK.
Install the SDK #
npm install @releva-ai/sdk-react-native
# or
yarn add @releva-ai/sdk-react-native
Then the peer dependencies:
npm install @react-native-async-storage/async-storage \
@react-native-community/netinfo \
@notifee/react-native \
@react-native-firebase/app \
@react-native-firebase/messaging
On iOS, run cd ios && pod install && cd .. afterwards.
All Firebase examples below use the modular API of @react-native-firebase/* v22 and later (getMessaging(), getToken(), โฆ). The older namespaced API was removed in v26, which is what a fresh install resolves today.
Choose a notification adapter #
The SDK core depends on no notification library. Displaying Releva’s data-only pushes and reporting taps is the job of a notification adapter, shipped as a subpath import so only the one you use reaches your bundle.
| Adapter | Import | Stack | What you get |
|---|---|---|---|
| notifee (recommended) | @releva-ai/sdk-react-native/notifee | @react-native-firebase/messaging + @notifee/react-native | The full feature set on both platforms: rich images, action buttons, engagement tracking, inbox sync. |
| expo-notifications | @releva-ai/sdk-react-native/expo | expo-notifications | Title, body and button actions on both platforms; no big-picture images on Android. iOS push needs APNs import enabled on your Releva account. |
Register the adapter once, at module top level โ in index.js, outside any component โ so it is in place before any message handler can run:
// index.js
import { EngagementTrackingService } from '@releva-ai/sdk-react-native';
import { notifeeNotificationAdapter } from '@releva-ai/sdk-react-native/notifee';
EngagementTrackingService.setNotificationAdapter(notifeeNotificationAdapter);
Tracking, banners, stories, NPS and the inbox need no adapter at all โ it is only for displaying push notifications.
Set up Firebase #
Required only if you plan to use Releva to send push notifications.
Android: add your google-services.json to android/app/, then wire the Google services plugin:
// android/build.gradle
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
// android/app/build.gradle
apply plugin: 'com.google.gms.google-services'
iOS: add your GoogleService-Info.plist to the Xcode project.
Initialize the Releva Client #
import { RelevaClient, NavigationService } from '@releva-ai/sdk-react-native';
import {
AuthorizationStatus,
getMessaging,
getToken,
onTokenRefresh,
requestPermission,
} from '@react-native-firebase/messaging';
import { Platform } from 'react-native';
// realm - use '' unless instructed otherwise by your account manager
// accessToken - use the access token provided by your account manager
const client = new RelevaClient('', '<yourAccessToken>');
async function initializeSDK() {
// Set the deviceId based on your current logic for device tracking
await client.setDeviceId('<deviceId>');
// If a user has registered or logged in, provide a profileId
await client.setProfileId('<profileId>');
// Enable push engagement metrics.
// IMPORTANT: set the profileId and deviceId first!
await client.enablePushEngagementTracking({
onNotificationTapped: (action) => {
// Your app decides how to navigate - see below
},
});
await registerPushToken();
}
async function registerPushToken() {
const messaging = getMessaging();
const authStatus = await requestPermission(messaging);
const enabled =
authStatus === AuthorizationStatus.AUTHORIZED ||
authStatus === AuthorizationStatus.PROVISIONAL;
if (!enabled) return;
const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
const token = await getToken(messaging);
if (token) {
await client.registerPushToken(deviceType, token);
}
onTokenRefresh(messaging, async (newToken) => {
await client.registerPushToken(deviceType, newToken);
});
}
If you use React Navigation, hand the SDK your navigation ref so the notification-tap callback can reach it:
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
import { NavigationService } from '@releva-ai/sdk-react-native';
const navigationRef = createNavigationContainerRef();
NavigationService.setNavigationRef(navigationRef);
function App() {
return (
<NavigationContainer ref={navigationRef}>
{/* Your screens */}
</NavigationContainer>
);
}
Call client.dispose() when your root component unmounts. It stops the inbox’s AppState listener and the engagement batch timer, which otherwise leak across hot reloads in development:
useEffect(() => {
client.initializeInbox();
return () => client.dispose();
}, []);
Identify the visitor #
The SDK identifies a user solely by the profileId you set. Contact details and other profile attributes are never sent from the app โ send those from your backend through the Create / Update Profile API.
On login, set the profile id normally and the anonymous profile the user browsed under is merged into it:
await client.setProfileId(loggedInUserId);
On logout, generate a fresh anonymous id and pass true to skip that merge, then re-register the push token against the new profile:
import { v4 as uuidv4 } from 'uuid';
const newAnonymousProfileId = uuidv4();
await client.setProfileId(newAnonymousProfileId, true);
const token = await getToken(getMessaging());
if (token) {
const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
await client.registerPushToken(deviceType, token);
}
Push notifications #
Releva sends data-only FCM messages, so the operating system displays nothing by itself. Two pieces of wiring are required, or push works in the foreground and nowhere else.
1. Background message handler #
Register it at module top level, in index.js, outside any React component โ on a cold start that file runs before your app initialises, so the adapter has to be registered there too:
// index.js
import { getMessaging, setBackgroundMessageHandler } from '@react-native-firebase/messaging';
import { EngagementTrackingService } from '@releva-ai/sdk-react-native';
import { notifeeNotificationAdapter } from '@releva-ai/sdk-react-native/notifee';
EngagementTrackingService.setNotificationAdapter(notifeeNotificationAdapter);
setBackgroundMessageHandler(getMessaging(), async (remoteMessage) => {
await EngagementTrackingService.handleBackgroundMessage(remoteMessage.data ?? {});
});
Without this, notifications that arrive while the app is backgrounded or killed are never displayed at all.
2. Cold-start tap check #
When a user taps a notification while the app is killed, the tap arrives before your JavaScript exists. Once enablePushEngagementTracking() has run and your navigation container is ready, call:
await client.checkInitialNotification();
It resolves immediately when the app was not launched from a notification. When it was, the tap is tracked and your onNotificationTapped callback fires exactly as it would for a warm tap. Do not call it inside your SDK initialisation โ the navigation ref is not ready yet at that point.
Handling the tap #
await client.enablePushEngagementTracking({
onNotificationTapped: (action) => {
// action.target - 'screen', 'url', 'inbox', or null
// action.screen - app-defined screen name (when target is 'screen')
// action.url - URL to open (when target is 'url')
// action.parameters - parsed navigation parameters
const navRef = NavigationService.navigationRef;
if (action.target === 'inbox') {
navRef?.current?.navigate('Inbox', action.parameters);
} else if (action.target === 'url' && action.url) {
Linking.openURL(action.url);
} else if (action.target === 'screen' && action.screen) {
// action.screen is the free-form name configured in the Releva dashboard
navRef?.current?.navigate(action.screen, action.parameters);
}
},
});
Track what the visitor is doing #
The high-level methods cover the common screens. Each returns the recommenders and banners configured for that screen token.
// Screen view
const response = await client.trackScreenView({
screenToken: 'home_screen',
productIds: ['prod1', 'prod2', 'prod3'],
categories: ['electronics', 'phones'],
locale: 'en',
currency: 'USD',
});
// Product view
await client.trackProductView({
screenToken: 'product_detail',
productId: 'product-123',
categories: ['electronics', 'phones'],
});
// Search
await client.trackSearchView({
screenToken: 'search_results',
query: 'red running shoes',
resultProductIds: ['prod1', 'prod2', 'prod3'],
filter: NestedFilter.and([
SimpleFilter.priceRange(50, 200),
SimpleFilter.brand('Nike'),
SimpleFilter.color('red'),
]),
});
// Checkout - the user is identified by the profileId you already set
const orderedCart = createPaidCart(cartProducts, orderId);
await client.trackCheckoutSuccess({
screenToken: 'checkout_success',
orderedCart,
});
Cart and wishlist #
import {
createActiveCart,
emptyCustomFields,
} from '@releva-ai/sdk-react-native';
await client.setCart(createActiveCart([
{ id: '<productId>', price: 29.99, quantity: 1, custom: emptyCustomFields() },
]));
await client.setWishlist([
{ id: '<productId>', custom: emptyCustomFields() },
]);
On-screen content #
Banners, stories, NPS surveys and the App Inbox are configured in the Releva dashboard and arrive in the response to a tracking call. Wrap the screens where they should appear.
import {
BannerDisplayWidget,
StoryDisplayWidget,
NpsOverlayWidget,
} from '@releva-ai/sdk-react-native';
function HomeScreen() {
return (
<BannerDisplayWidget
targetSelector="#home-content"
client={client}
onLinkTap={(url) => Linking.openURL(url)}
>
<StoryDisplayWidget client={client} onLinkTap={(url) => Linking.openURL(url)}>
{/* Your screen content */}
</StoryDisplayWidget>
</BannerDisplayWidget>
);
}
// NPS goes around your whole app
function App() {
return (
<NpsOverlayWidget
onSubmit={async (token, score, comment) => {
await client.submitNpsResponse({ token, score, comment });
}}
>
<NavigationContainer>{/* Your screens */}</NavigationContainer>
</NpsOverlayWidget>
);
}
Banner and story triggers are set in the dashboard: immediately, delaySeconds, scrollPercentage, cartChanged and wishlistChanged. Leave-intent is a web-only trigger and never fires on mobile.
NPS surveys can also be triggered or cancelled by your own events:
client.trackEvent('checkout_complete'); // may trigger a survey
client.trackEvent('checkout_started'); // may cancel a pending one
App Inbox #
Initialise the inbox after the profile id is set, then read and observe its state:
import { InboxService } from '@releva-ai/sdk-react-native';
await client.setProfileId('<profileId>');
await client.initializeInbox();
const state = InboxService.state; // messages, unreadCount, isLoading, hasMore
InboxService.addListener((newState) => {
// Update your UI
});
await InboxService.refresh(); // first page + unread count
Configuration options #
import { RelevaClient, RelevaConfigPresets } from '@releva-ai/sdk-react-native';
// Full functionality (default)
new RelevaClient('', '<yourAccessToken>');
// Tracking only
new RelevaClient('', '<yourAccessToken>', RelevaConfigPresets.trackingOnly());
// With custom timeouts and retries
new RelevaClient('', '<yourAccessToken>', {
...RelevaConfigPresets.full(),
requestTimeoutMs: 30000, // default
maxRetryAttempts: 3, // default - 4 attempts in total
transportRetryDelayMs: 1000, // default
serverErrorRetryDelayMs: 2000, // default
});
Tracking calls, banner and story events, push token registration and NPS submissions are all aborted after requestTimeoutMs and retried on a transport failure or a 5xx โ never on a 4xx. Inbox calls are not on that path: they make a single request with no deadline and no retry.
Backend calls you still need #
The app covers what the user does. Your catalogue, your orders and your consent changes still have to reach Releva from your backend:
- Call the Product Create / Update API whenever your catalogue changes.
- Implement the Periodical Product Sync as a fallback, so a missed update cannot leave Releva stale.
- Call the Cart Paid API when a customer places an order, and the Refund API when one is refunded.
- Call the Subscribe and Unsubscribe APIs when a user changes their marketing preferences.
- Call the Profile Register API when a new user registers.
All set! #
Your app is integrated. Explore the API Specification for the server-side calls that let you send custom product and profile data, which can then be used in personalization.