npm package: @releva-ai/sdk-react-native
Description: Releva.ai SDK for React Native apps
Latest version: 0.0.3

# Releva React Native SDK

This package offers an easy way to integrate Releva's AI-powered e-commerce personalization platform into your mobile app built on React Native.

## Features

### E-commerce Personalization
- **Product Recommendations**: AI-powered product suggestions with real-time personalization
- **Dynamic Content**: Personalized banners, stories, and content blocks based on user behavior
- **Advanced Filtering**: Complex product filtering with nested AND/OR logic, price ranges, custom fields
- **Smart Search**: Search tracking with result optimization and recommendation integration

### Mobile Tracking & Analytics
- **Screen Tracking**: Integration with React Navigation for seamless route/screen tracking
- **E-commerce Events**: Product views, cart changes, checkout tracking, search analytics
- **Custom Events**: Flexible event system for business-specific tracking needs

### Push Notifications
- **Firebase Integration**: Complete FCM push notification system with data-only payloads
- **Structured Navigation**: `RelevaNotificationAction` type with parsed target, screen, URL, and parameters
- **Engagement Analytics**: Delivered, opened, dismissed tracking
- **Deep Linking**: Route users directly to screens or inbox messages from notification taps
- **Cross-Platform**: Android and iOS device support

### App Inbox
- **Persistent Messages**: In-app message centre where messages survive until read, deleted, or expired
- **Rich Content**: Messages rendered with Unlayer design JSON, fully personalized per user
- **Real-Time Sync**: Silent push notifications trigger automatic inbox refresh via `handleForegroundMessage`
- **Deep Link to Message**: `getMessageById()` enables navigating directly to a specific inbox message from a push notification
- **Optimistic Updates**: Mark read, mark all read, and delete with instant UI feedback and rollback on failure
- **Cursor Pagination**: Efficient infinite scroll with server-side cursor pagination
- **Local Caching**: Messages cached locally for instant display on return visits
- **Smart Refresh**: `refreshIfStale()` avoids unnecessary network requests on app resume (5-minute TTL)

### NPS Surveys
- **Server-Driven**: NPS survey configuration and targeting managed from the Releva dashboard
- **Custom Events**: Trigger surveys or cancel pending surveys based on app events

### Flexible Configuration
- **Modular Setup**: Enable only needed features via `RelevaConfigPresets`
- **Endpoint Override**: Use `setEndpointOverride()` for local development with ngrok or custom API endpoints
- **Offline Support**: Engagement events are queued and sent when connection is available

## Installation

Add the Releva React Native SDK to your project:

```bash
npm install @releva-ai/sdk-react-native
# or
yarn add @releva-ai/sdk-react-native
```

### Peer Dependencies

Install the required peer dependencies:

```bash
npm install @react-native-async-storage/async-storage @react-native-firebase/messaging @react-native-community/netinfo
```

For iOS, install CocoaPods:

```bash
cd ios && pod install && cd ..
```

## Firebase Setup (Required for Push Notifications)

### 1. Install Firebase dependencies

```bash
npm install @react-native-firebase/app @react-native-firebase/messaging
```

### 2. Android Configuration

Add your `google-services.json` to `android/app/`.

In `android/build.gradle`:
```groovy
buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.4.0'
    }
}
```

In `android/app/build.gradle`:
```groovy
apply plugin: 'com.google.gms.google-services'
```

### 3. iOS Configuration

Add your `GoogleService-Info.plist` to the iOS project via Xcode.

## Initialize the Releva Client

```typescript
import { RelevaClient, NavigationService } from '@releva-ai/sdk-react-native';
import messaging from '@react-native-firebase/messaging';
import { Platform } from 'react-native';
import { v4 as uuidv4 } from 'uuid';

// Initialize Releva Client
// 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 device ID 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 app push notification engagement metrics collection
  // IMPORTANT: ensure that you have set the profileId and deviceId first!
  await client.enablePushEngagementTracking({
    onNotificationTapped: (action) => {
      // Handle navigation — see "Push Notification Engagement Tracking" below
    },
  });

  // Register FCM token
  await registerPushToken();
}

async function registerPushToken() {
  const authStatus = await messaging().requestPermission();
  const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;

  if (enabled) {
    const token = await messaging().getToken();
    if (token) {
      const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
      await client.registerPushToken(deviceType, token);
    }

    // Handle token refresh
    messaging().onTokenRefresh(async (newToken) => {
      const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
      await client.registerPushToken(deviceType, newToken);
    });
  }
}
```

### React Navigation Integration

```typescript
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
import { NavigationService } from '@releva-ai/sdk-react-native';

const navigationRef = createNavigationContainerRef();

// Optional: store the navigation ref in NavigationService so your
// onNotificationTapped callback can access it via
// NavigationService.navigationRef.current
NavigationService.setNavigationRef(navigationRef);

function App() {
  return (
    <NavigationContainer ref={navigationRef}>
      {/* Your screens */}
    </NavigationContainer>
  );
}
```

## Endpoint Override (Local Development)

For local development or testing with a custom API endpoint (e.g. ngrok):

```typescript
// Set a custom endpoint (overrides realm-based URL)
client.setEndpointOverride('https://your-ngrok-url.ngrok.io');

// Clear override to revert to default
client.setEndpointOverride(null);
```

The override applies to all API calls including inbox. Pass `null` to clear and revert to the realm-based default.

> **Warning:** Non-HTTPS endpoints will log a warning. Use HTTPS in production.

## Profile Management and User Logout

### Important: Handling User Logout

When a user logs out, prevent merging the logged-in user's profile with the anonymous profile:

```typescript
// When user logs out, generate a new anonymous profile ID
const newAnonymousProfileId = uuidv4();

// Set the new profile ID with skipMerge set to true
await client.setProfileId(newAnonymousProfileId, true);

// Re-register the firebase push token to the new profile
const token = await messaging().getToken();
if (token) {
  const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
  await client.registerPushToken(deviceType, token);
}
```

**Default Behavior (when logging in or switching users):**
```typescript
// Normal profile change - previous profile will be merged
await client.setProfileId(loggedInUserId);
```

## Push Notification Engagement Tracking

### Option 1: Use the SDK's built-in tracking (Recommended)

The SDK handles all Firebase messaging hooks and engagement tracking. Your app
provides a required `onNotificationTapped` callback that receives a structured
`RelevaNotificationAction` — **your app decides how to navigate**. This keeps
the SDK compatible with any navigation approach (React Navigation, Expo Router,
custom routing, etc.).

```typescript
import { RelevaNotificationAction, NavigationService } from '@releva-ai/sdk-react-native';
import { Linking } from 'react-native';

await client.enablePushEngagementTracking({
  onNotificationTapped: (action: RelevaNotificationAction) => {
    // 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 parameters from navigate_to_parameters JSON

    // Example using React Navigation (adapt to your routing solution):
    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 screen name configured in the
      // Releva dashboard — map it to your app's screen names.
      navRef?.current?.navigate(action.screen, action.parameters);
    }
  },
});
```

### Checking Releva Messages

```typescript
// Check whether a notification data payload originated from Releva
const isReleva = RelevaClient.isRelevaMessage(remoteMessage.data);
```

### Inbox Sync from Silent Push

When a new inbox message is delivered, the server sends a silent push with `inbox_sync: "true"`. Set up a foreground message listener to handle these signals:

```typescript
import messaging from '@react-native-firebase/messaging';
import { EngagementTrackingService } from '@releva-ai/sdk-react-native';

messaging().onMessage(async (remoteMessage) => {
  await EngagementTrackingService.handleForegroundMessage(
    remoteMessage.data ?? {},
  );
});
```

When the app resumes from background, the inbox automatically refreshes if the cache is stale (older than 5 minutes). This serves as a reliable fallback when silent push doesn't arrive (battery optimization, iOS background limits, etc.).

## Send Push Requests

### Set Wishlist

```typescript
import { WishlistProduct, emptyCustomFields } from '@releva-ai/sdk-react-native';

const product: WishlistProduct = { id: '<productId>', custom: emptyCustomFields() };
await client.setWishlist([product]);
```

### Set Cart

```typescript
import { CartProduct, Cart, createActiveCart, emptyCustomFields } from '@releva-ai/sdk-react-native';

const product: CartProduct = {
  id: '<productId>',
  price: 29.99,
  quantity: 1,
  custom: emptyCustomFields(),
};
await client.setCart(createActiveCart([product]));
```

## High-level Tracking Methods

### Screen View Tracking

```typescript
const response = await client.trackScreenView({
  screenToken: 'home_screen',
  productIds: ['prod1', 'prod2', 'prod3'],
  categories: ['electronics', 'phones'],
  locale: 'en',
  currency: 'USD',
});
```

### Product View Tracking

```typescript
const response = await client.trackProductView({
  screenToken: 'product_detail',
  productId: 'product-123',
  categories: ['electronics', 'phones'],
  locale: 'en',
  currency: 'USD',
});
```

### Search Tracking

```typescript
import { SimpleFilter, NestedFilter } from '@releva-ai/sdk-react-native';

const response = 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'),
  ]),
  locale: 'en',
  currency: 'USD',
});
```

### Checkout Success Tracking

```typescript
import { createPaidCart } from '@releva-ai/sdk-react-native';

const orderedCart = createPaidCart(cartProducts, orderId);
const response = await client.trackCheckoutSuccess({
  screenToken: 'checkout_success',
  orderedCart,
  userEmail: 'user@example.com',
  locale: 'en',
  currency: 'USD',
});
```

## Banners

Wrap your screen content with `BannerDisplayWidget` to enable banner display:

```tsx
import { BannerDisplayWidget } from '@releva-ai/sdk-react-native';

function HomeScreen() {
  const client = getRelevaClient();

  return (
    <BannerDisplayWidget
      targetSelector="#home-content"
      client={client}
      onLinkTap={(url) => Linking.openURL(url)}
    >
      {/* Your screen content */}
    </BannerDisplayWidget>
  );
}
```

### Banner Triggers

- **immediately**: Shows as soon as the screen loads
- **delaySeconds**: Shows after a specified delay
- **scrollPercentage**: Shows when user scrolls to a certain percentage
- **cartChanged**: Shows when cart is modified
- **wishlistChanged**: Shows when wishlist is modified

## Stories

Wrap your screen content with `StoryDisplayWidget`:

```tsx
import { StoryDisplayWidget } from '@releva-ai/sdk-react-native';

function HomeScreen() {
  return (
    <StoryDisplayWidget
      client={client}
      onLinkTap={(url) => Linking.openURL(url)}
    >
      {/* Your screen content */}
    </StoryDisplayWidget>
  );
}
```

## App Inbox

### Initialize the Inbox

```typescript
await client.setProfileId('<profileId>');
await client.initializeInbox();
```

> **Cleanup**: Call `client.dispose()` when your root component unmounts (e.g. in a `useEffect` cleanup). This stops the inbox's AppState listener and the engagement tracking batch timer, preventing leaks in development hot-reloads and during testing.
>
> ```typescript
> useEffect(() => {
>   client.initializeInbox();
>   return () => client.dispose();
> }, []);
> ```

### Access Inbox State

```typescript
import { InboxService } from '@releva-ai/sdk-react-native';

const state = InboxService.state;
console.log(state.messages);     // InboxMessage[]
console.log(state.unreadCount);  // number
console.log(state.isLoading);    // boolean

// Listen for changes
InboxService.addListener((newState) => {
  // Update your UI
});
```

### Refresh and Pagination

```typescript
await InboxService.refresh();        // Fetch first page + unread count
await InboxService.loadMore();       // Fetch next page (cursor pagination)
await InboxService.refreshIfStale(); // Only refresh if cache is older than 5 minutes
```

### Mark as Read and Delete

All operations use optimistic updates — the UI updates instantly while the API call happens in the background. If the API call fails, the change is rolled back.

```typescript
await InboxService.markAsRead(message.id);
await InboxService.markAllAsRead();
await InboxService.deleteMessage(message.id);
```

### Track Message Action

Fire-and-forget tracking for when a user interacts with a message link or button:

```typescript
await InboxService.trackAction(message.id);
```

### Navigate to a Specific Inbox Message (from Push)

When a push notification targets a specific inbox message, use `getMessageById` to look it up by its template ID:

```typescript
import { InboxService, RelevaNotificationAction } from '@releva-ai/sdk-react-native';

await client.enablePushEngagementTracking({
  onNotificationTapped: async (action: RelevaNotificationAction) => {
    if (action.target === 'inbox') {
      const inboxMessageId = Number(action.parameters?.inboxMessageId);
      if (inboxMessageId) {
        const message = await InboxService.getMessageById(inboxMessageId);
        if (message) {
          if (!message.read) await InboxService.markAsRead(message.id);
          navigation.navigate('InboxMessage', { messageId: message.id });
          return;
        }
      }
      navigation.navigate('Inbox');
    }
  },
});
```

`getMessageById` calls `refreshIfStale()` internally before searching, so in the typical flow where a silent push has already refreshed the inbox, no extra network request is made.

### Render Message Content

```tsx
import { InboxMessageWidget } from '@releva-ai/sdk-react-native';

<InboxMessageWidget
  message={inboxMessage}
  onLinkTap={(url) => Linking.openURL(url)}
/>
```

### Handle Inbox Sync Signal

See [Inbox Sync from Silent Push](#inbox-sync-from-silent-push) above for setting up the foreground message listener that keeps the inbox in sync.

## NPS Surveys

### Setup

Wrap your app with `NpsOverlayWidget`:

```tsx
import { NpsOverlayWidget } from '@releva-ai/sdk-react-native';

function App() {
  return (
    <NpsOverlayWidget
      onSubmit={async (token, score, comment) => {
        await client.submitNpsResponse({ token, score, comment });
      }}
    >
      <NavigationContainer>
        {/* Your screens */}
      </NavigationContainer>
    </NpsOverlayWidget>
  );
}
```

### Firing custom events

```typescript
// Trigger NPS after checkout
client.trackEvent('checkout_complete');

// Cancel pending NPS if user enters a sensitive flow
client.trackEvent('checkout_started');
```

## Configuration Options

```typescript
import { RelevaClient, RelevaConfigPresets } from '@releva-ai/sdk-react-native';

// Full functionality (default)
new RelevaClient('', 'token');

// Custom configuration
new RelevaClient('', 'token', RelevaConfigPresets.trackingOnly());
```

## API Reference

### RelevaClient Methods

#### Configuration
- `setDeviceId(deviceId: string)` - Set unique device identifier
- `setProfileId(profileId: string, skipMerge?: boolean)` - Set user profile identifier. Pass `skipMerge: true` on logout to prevent merging with the previous profile.
- `setAppVersion(version: string)` - Set app version for analytics context
- `setEndpointOverride(url: string | null)` - Override API endpoint URL for all calls including inbox. Pass `null` to clear.
- `setCart(cart: Cart)` - Update user's cart
- `setWishlist(products: WishlistProduct[])` - Update user's wishlist
- `clearCartStorage()` - Clear stored cart data

#### Push Notifications
- `enablePushEngagementTracking({ onNotificationTapped })` - Enable push notification tracking with required tap callback. The app handles all navigation.
- `registerPushToken(type: DeviceType, token: string)` - Register FCM token with Releva

#### Tracking
- `push(request: PushRequest)` - Send custom tracking request
- `trackScreenView(options)` - Track screen views
- `trackProductView(options)` - Track product views
- `trackSearchView(options)` - Track search queries
- `trackCheckoutSuccess(options)` - Track successful purchases
- `trackCustomEvent(options)` - Track custom events
- `trackEvent(eventName: string)` - Fire a named event (for NPS triggers)

#### Inbox
- `initializeInbox()` - Initialize the inbox service (call after `setProfileId`)
- `inbox` - Property returning the `InboxService` singleton

#### Analytics
- `bannerImpression(banner)` - Track banner display
- `bannerAction(banner, action?)` - Track banner interaction
- `storyImpression(story)` - Track story display
- `storyAction(story, action, slideId?)` - Track story interaction

#### NPS
- `submitNpsResponse(options)` - Submit NPS survey response

#### Utility
- `static isRelevaMessage(data)` - Check if a notification data payload originated from Releva (matches `click_action` starting with `RELEVA_`)
- `dispose()` - Release resources held by the client

### InboxService Methods

- `state` - Current `InboxState` (messages, unreadCount, isLoading, hasMore, nextCursor, lastFetchTime)
- `addListener(listener)` / `removeListener(listener)` - Subscribe to state changes
- `initialize(options)` - Initialize with access token, realm, and optional endpoint override
- `setEndpointOverride(url)` - Update the API endpoint for inbox calls
- `refresh()` - Fetch first page of messages and unread count
- `loadMore()` - Fetch next page using cursor pagination
- `markAsRead(messageId)` - Mark a single message as read (optimistic)
- `markAllAsRead()` - Mark all messages as read (optimistic)
- `deleteMessage(messageId)` - Delete a message (optimistic)
- `trackAction(messageId)` - Track message interaction (fire-and-forget)
- `getMessageById(inboxMessageId)` - Look up a message by template ID (refreshes if stale)
- `handleSyncSignal()` - Trigger a full inbox refresh (called by silent push handler)
- `refreshIfStale()` - Refresh only if cache is older than 5 minutes
- `dispose()` - Remove lifecycle observer

### EngagementTrackingService Methods

- `static isRelevaMessage(data)` - Check if notification data is from Releva
- `handleNotificationOpened(data)` - Process a notification tap (tracking + callback)
- `handleForegroundMessage(data)` - Process a foreground FCM message (inbox sync + notification display)
- `handleBackgroundMessage(data)` - Display data-only notifications in background handler
- `forceSendPendingEvents()` - Immediately send any queued engagement events
- `dispose()` - Stop the batch timer

### Types

#### RelevaNotificationAction
```typescript
interface RelevaNotificationAction {
  target: string | null;              // 'screen', 'url', 'inbox', or null
  screen: string | null;              // App-defined screen name (when target is 'screen').
                                      // Free-form value configured in the Releva dashboard.
  url: string | null;                 // URL to open
  parameters: Record<string, any>;    // Parsed from navigate_to_parameters JSON
}
```

#### InboxMessage
```typescript
interface InboxMessage {
  id: string;                         // Unique delivery ID
  title: string;                      // Message title
  design: Record<string, any>;        // Unlayer design JSON
  read: boolean;                      // Read status
  createdAt: Date;                    // Delivery timestamp
  inboxMessageId: number;             // Source message template ID
}
```

## Additional Information

For additional information, please visit [https://releva.ai](https://releva.ai) or contact tech-support@releva.ai