View Categories

React Native (Android / iOS)

7 min read

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.

AdapterImportStackWhat you get
notifee (recommended)@releva-ai/sdk-react-native/notifee@react-native-firebase/messaging + @notifee/react-nativeThe full feature set on both platforms: rich images, action buttons, engagement tracking, inbox sync.
expo-notifications@releva-ai/sdk-react-native/expoexpo-notificationsTitle, 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:

  1. Call the Product Create / Update API whenever your catalogue changes.
  2. Implement the Periodical Product Sync as a fallback, so a missed update cannot leave Releva stale.
  3. Call the Cart Paid API when a customer places an order, and the Refund API when one is refunded.
  4. Call the Subscribe and Unsubscribe APIs when a user changes their marketing preferences.
  5. 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.