This article explains how to perform a full integration of a React Native app with Releva, using the Releva React Native SDK.
Push notifications work with either of two stacks: Firebase messaging + notifee (recommended, described in this article) or expo-notifications, for Expo apps that keep their existing push stack. If your app uses expo-notifications, follow Push notifications with Expo for installation and push, and this article for everything else.
Install the SDK #
npm install @releva-ai/sdk-react-native
# or
yarn add @releva-ai/sdk-react-native
Then the peer dependencies for the Firebase messaging + notifee stack (Expo apps: see Push notifications with Expo instead, and do not install these):
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.
No crypto polyfill is needed: the SDK does not require react-native-get-random-values or a global crypto.
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. On iOS, images and buttons appear on notifications that arrive while the app is open; iOS draws the rest itself. |
| expo-notifications | @releva-ai/sdk-react-native/expo | expo-notifications + expo-task-manager | Title and body on both platforms, and action buttons on Android. No images on Android; on iOS, buttons and images need a notification service extension in your app. iOS push needs APNs import enabled on your Releva account. See Push notifications with Expo. |
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 #
The push token code below is for the Firebase messaging stack. On the Expo stack, register the token as described in Push notifications with Expo; the rest of the client setup is the same.
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. The example uses uuid, which is your app’s dependency, not the SDK’s; it needs react-native-get-random-values imported on the first line of index.js, because Hermes has no crypto. Any other source of a unique string works as well.
npm install uuid react-native-get-random-values
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. Three pieces of wiring are required. On the Expo stack, follow Push notifications with Expo instead of this section.
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 on Android while the app is backgrounded or killed are never displayed. (iOS draws those itself — see below.)
2. Foreground messages #
A push that arrives while the app is open goes to onMessage, not to the background handler. Hand it to the SDK: it displays the notification, and when the push is a silent inbox-sync signal (inbox_sync: "true"), it refreshes the inbox instead.
import { getMessaging, onMessage } from '@react-native-firebase/messaging';
import { EngagementTrackingService } from '@releva-ai/sdk-react-native';
onMessage(getMessaging(), async (remoteMessage) => {
await EngagementTrackingService.handleForegroundMessage(remoteMessage.data ?? {});
});
3. 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);
}
},
});
On iOS #
Releva sends every visible iOS push with an alert, so while your app is in the background or closed, iOS draws the notification itself. Taps on those notifications are still tracked and routed with no extra wiring: through checkInitialNotification() when the app was closed, and straight to onNotificationTapped when it was in the background. Do not also forward Releva taps from your own onNotificationOpenedApp() handler, or each tap is counted twice.
A notification iOS draws shows title and body only: an action button or an image needs a notification service extension in your app (native code). Serve campaign images as JPEG or PNG — iOS never shows WebP.
Track what the visitor is doing #
The high-level methods cover the common screens. Each returns the recommenders, banners and stories 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 (
<StoryDisplayWidget client={client} onLinkTap={(url) => Linking.openURL(url)}>
<BannerDisplayWidget
targetSelector="#home-content"
client={client}
onLinkTap={(url) => Linking.openURL(url)}
>
{({ header, footer }) => (
<ScrollView>
{header}
{/* Your screen content */}
{footer}
</ScrollView>
)}
</BannerDisplayWidget>
</StoryDisplayWidget>
);
}
// 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>
);
}
Static banners (placed inline at targetSelector) are drawn through the header and footer that BannerDisplayWidget passes to its children; popups, flyouts and bars need nothing more.
Feed the widgets from your screen views #
The widgets show what your app hands them; the SDK does not do this on its own. Create one BannerManagerService and one StoryManagerService for the app and pass them each screen view’s banners and stories. Without this step, no banner or story ever appears.
import { BannerManagerService, StoryManagerService } from '@releva-ai/sdk-react-native';
export const bannerManager = new BannerManagerService();
export const storyManager = new StoryManagerService();
// On each screen view. screenToken is the page token from the Releva dashboard;
// without it the response carries no banners or stories.
const response = await client.trackScreenView({ screenToken: '<page token>' });
if (response.banners.length > 0) bannerManager.initialize(response.banners);
if (response.stories.length > 0) storyManager.initialize(response.stories);
Banner and story triggers are set in the dashboard: immediately, delaySeconds, scrollPercentage, cartChanged and wishlistChanged. Three of them need a signal from your app, sent to both managers:
scrollPercentage: report the scroll from the screen’sonScrollwithbannerManager.setScrollOffset(offset, maxOffset)(and the same onstoryManager).cartChanged: callnotifyCartChanged()afterclient.setCart().wishlistChanged: callnotifyWishlistChanged()afterclient.setWishlist().
Leave-intent is a web-only trigger and never fires on mobile.
On Android apps drawn edge to edge (enforced on Android 15 and later for apps that target Android 15, API 35, as Expo SDK 57 apps do), popups, flyouts and stories keep their content and close button below the status bar. A bar or card anchored to the bottom of the screen can still sit on a 3-button navigation bar.
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
New messages are signalled by a silent push, which the foreground handler from step 2 above passes to the inbox. When the app resumes, a cache older than five minutes is refreshed as well, as a fallback for a silent push that never arrived.
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.