# React Native (1.2.0)
> React Native SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (React Navigation, events & Action Intelligence)
URL: /embed/integration/react-native/v1.2.0
Markdown: /embed/integration/react-native/v1.2.0.md
# React Native Integration Guide [#react-native-integration-guide]
The complete guide for adding the RevRag embedded voice agent ("AI Saathi") to a React Native app.
| | |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Package** | `@revrag-ai/embed-react-native` |
| **SDK version** | **1.2.0** on [npm](https://www.npmjs.com/package/@revrag-ai/embed-react-native) · what changed: [release notes](/embed/integration/react-native/react-native-releases) |
| **React Native** | **0.76 – 0.86** (reference: 0.78, Hermes) |
| **API key** | Get it from [https://app.revrag.ai](https://app.revrag.ai) |
**How this guide works**
| | |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| **Order** | **Basic → intermediate → advanced** |
| **Every feature** | What is it? → Why do I need it? → How do I configure it? → Example → Common problem → How to fix it |
| **SDK internals** | You do **not** need to know how the SDK works internally to follow this |
**A working widget takes five steps**, all in [1. Basic Integration](#1-basic-integration):
| Step | What you do | Details |
| ------------------- | ----------------------------------------- | ----------------------------------------------------------- |
| 1. **Install** | Install the SDK and its dependencies | [Step 1](#step-1--install-the-sdk-and-its-dependencies) |
| 2. **Native setup** | Required, one time | [Step 2](#step-2--native-setup-required-one-time) |
| 3. **Initialize** | Initialize the SDK once, at the app root | [Step 3](#step-3--initialize-the-sdk-once-at-the-app-root) |
| 4. **Wrap** | Wrap your app and add the widget | [Step 4](#step-4--wrap-your-app-and-add-the-widget) |
| 5. **Identify** | Tell the SDK who the user is, after login | [Step 5](#step-5--tell-the-sdk-who-the-user-is-after-login) |
**If you only read one section, read [1. Basic Integration](#1-basic-integration).** That alone gets a working widget on screen.
***
## Table of contents [#table-of-contents]
1. [Basic Integration](#1-basic-integration) — install, native setup, provider, first call
2. [Configuration](#2-configuration) — `useInitialize` and `EmbedProvider` options
3. [Screen & Navigation](#3-screen--navigation) — how the SDK knows where the user is
4. [Widget Visibility](#4-widget-visibility) — where the button shows, and why it matters
5. [Event Capturing](#5-event-capturing) — data events, click tracking, agent lifecycle
6. [Action Intelligence / UI Capture](#6-action-intelligence--ui-capture) — how the agent sees and acts
7. [Advanced Integration](#7-advanced-integration) — manual control, groups, permissions
8. [Troubleshooting](#8-troubleshooting) — problem → why → fix
9. [Debugging Guide](#9-debugging-guide) — rule out one layer at a time
10. [Integration Examples](#10-integration-examples) — copy-paste patterns
11. [Best Practices](#11-best-practices)
***
## 1. Basic Integration [#1-basic-integration]
**Goal:** a floating voice-agent button on your screens that a user can tap to talk to the agent. There are five steps.
### Step 1 — Install the SDK and its dependencies [#step-1--install-the-sdk-and-its-dependencies]
```bash
# The SDK (never 1.1.0 — it crashes every FlatList / SectionList)
npm install @revrag-ai/embed-react-native@^1.2.0
# Required peer dependencies (the SDK will not run without these)
npm install \
@livekit/react-native \
@livekit/react-native-webrtc \
@react-native-async-storage/async-storage \
react-native-gesture-handler \
react-native-reanimated \
react-native-linear-gradient \
lottie-react-native \
react-native-safe-area-context
# iOS only: install native pods
cd ios && pod install && cd ..
# iOS only: register the SDK's bundled fonts (run once)
npx react-native-asset
```
**Why these dependencies?** The agent is a live voice call (LiveKit + WebRTC), the widget is animated (reanimated, lottie, linear-gradient), it stores a little config on device (async-storage), and the button is draggable and safe-area aware (gesture-handler, safe-area-context). They are marked as *peer* dependencies so you control their exact versions.
**Read this before pinning versions.** Recommended version for every peer dependency, two verified copy-paste stacks (RN 0.78–0.81 and RN 0.82–0.86), the supported React Native matrix, and the dependency pitfalls that break calls — like a duplicate `livekit-client`.
### Step 2 — Native setup (required, one time) [#step-2--native-setup-required-one-time]
**Babel** — add the reanimated plugin as the **last** plugin in `babel.config.js`:
```js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// ...any other plugins first...
'react-native-reanimated/plugin', // MUST be last
],
};
```
**LiveKit native init is required** — skipping it makes the first call crash with `audioRecordSamplesDispatcher is not initialized!`.
Android — `android/app/src/main/java//MainApplication.kt`:
```kotlin
import com.livekit.reactnative.LiveKitReactNative
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
LiveKitReactNative.setup(this) // ← before React Native starts
// ...rest of onCreate...
}
}
```
iOS — `ios//AppDelegate.swift`:
```swift
import livekit_react_native // note the module name (lowercase, underscores)
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: ...) -> Bool {
LivekitReactNative.setup() // ← before React Native starts (lowercase "k")
// ...rest of setup...
return true
}
```
**iOS microphone permission** — add to `ios//Info.plist` (missing this **crashes** the app when a call starts):
```xml
NSMicrophoneUsageDescriptionThis app needs microphone access to talk to the AI agent.
```
**Android permissions — you add nothing.** The SDK's manifest merges `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS` and `BLUETOOTH_CONNECT` (LiveKit adds `INTERNET`, `CAMERA`, …), and the microphone runtime prompt is shown by the SDK during `startCall`. Voice-only app? You may strip camera: ``.
**Do not call LiveKit's `registerGlobals()` yourself** — `useInitialize` does it. A second call duplicates native event listeners (relevant if your app already uses LiveKit — keep your existing native `setup()`, don't add another, and tell RevRag).
After native changes, **rebuild fully** (`gradlew clean` / `pod install`) — a Metro reload is not enough.
### Step 3 — Initialize the SDK once, at the app root [#step-3--initialize-the-sdk-once-at-the-app-root]
**What:** `useInitialize` registers your app with the RevRag backend and loads the widget configuration. **Why:** nothing else works until this has run. **Where:** the top-level `App` component, once.
```tsx
import { useInitialize } from '@revrag-ai/embed-react-native';
function App() {
useInitialize({
apiKey: 'YOUR_API_KEY', // required — from RevRag
appVersion: '1.0.0', // required — your app's version (see section 2)
});
// ...
}
```
### Step 4 — Wrap your app and add the widget [#step-4--wrap-your-app-and-add-the-widget]
**What:** `EmbedProvider` is the wrapper that tracks the current screen and **renders the floating button for you** — you do **not** render the button yourself.
**Why the order matters:** it must sit *outside* your navigation container so it can see route changes, and *inside* `GestureHandlerRootView` + `SafeAreaProvider`. Put it **above** any portal / paper / bottom-sheet providers too, or the widget renders underneath them.
**`navigationRef` is required for the button to appear.** The SDK learns the current screen only from the navigation listener attached through this ref — with no ref (or a different ref than the one on `NavigationContainer`) the button never shows and `startCall()` fails. Mount the container within \~1 s of the provider (don't gate it behind async auth/font loading — gate *inside* it instead).
```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { EmbedProvider } from '@revrag-ai/embed-react-native';
import { NavigationContainer } from '@react-navigation/native';
import { useRef } from 'react';
export default function App() {
const navigationRef = useRef(null);
useInitialize({ apiKey: '…', appVersion: '1.0.0' });
return (
{/* appVersion lives in useInitialize (required); no need to repeat it here */}
{/* your navigator */}
);
}
```
That is enough to **see** the button. To let it actually place a **call**, do Step 5.
### Step 5 — Tell the SDK who the user is (after login) [#step-5--tell-the-sdk-who-the-user-is-after-login]
**What:** a `USER_DATA` event with your app's user id. **Why:** the agent needs to know which user it is helping. Until it's sent, every other event is dropped and a call's token request aborts (`[Embed Call] no app_user_id stored`). Note `startCall()` still resolves `true` in that case — treat `AGENT_CONVERSATION_STARTED` as the real signal. It is stored only once the backend accepts it, so if sent offline, send it again.
```tsx
import { Embed, EmbedEventKeys } from '@revrag-ai/embed-react-native';
// call this right after your user logs in
await Embed.Event(EmbedEventKeys.USER_DATA, { app_user_id: 'user_123' });
```
### Complete copy-paste example [#complete-copy-paste-example]
```tsx
import React, { useEffect, useRef } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import {
useInitialize, EmbedProvider, Embed, EmbedEventKeys,
} from '@revrag-ai/embed-react-native';
import HomeScreen from './HomeScreen';
import LoginScreen from './LoginScreen';
const Stack = createNativeStackNavigator();
export default function App() {
const navigationRef = useRef(null);
useInitialize({ apiKey: 'YOUR_API_KEY', appVersion: '1.0.0' });
// In a real app, send this after the user actually logs in.
useEffect(() => {
Embed.Event(EmbedEventKeys.USER_DATA, { app_user_id: 'user_123' });
}, []);
return (
);
}
```
**That's the whole minimum integration.** Everything below is optional refinement.
***
## 2. Configuration [#2-configuration]
Two places hold configuration: **`useInitialize(...)`** (credentials) and **``** (behavior). Backend-controlled settings (agent voice, colors, feature flags) are managed by RevRag remotely — you do not set those in code.
### `useInitialize` options [#useinitialize-options]
| Option | Required | Default | What it does / when to use |
| ------------ | -------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey` | ✅ | — | Your RevRag API key. Identifies your tenant. |
| `embedUrl` | optional | RevRag default | Your RevRag server URL. Set it if RevRag gave you a specific host. |
| `appVersion` | ✅ | — | Your app's version string, attached to every event and sync. **This is the canonical place to set it** (alongside `apiKey`/`embedUrl`), and it **takes priority over** the optional `EmbedProvider` `appVersion` prop. Use `require('./package.json').version`. |
### `EmbedProvider` props [#embedprovider-props]
| Prop | Required | Default | What it does / when to use |
| ------------------------------- | -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `children` | ✅ | — | Your app tree (must contain the `NavigationContainer`). |
| `appVersion` | optional | — | **Override only.** Set `appVersion` in `useInitialize` (required) — that value wins. Pass it here only if the version isn't available at the init call site. Omitting it everywhere logs a warning and falls back to the native build version. |
| `navigationRef` | ✅ (for the widget to show) | — | Your React Navigation ref — the **same one** passed to `NavigationContainer`. It is how the SDK learns the current screen; **without it the button never appears** and `startCall()` fails. Also enables the agent's **back** action. |
| `showOnAllScreens` | optional | `false` | Show the button on every screen except `excludeScreens`. The simplest setup. |
| `includeScreens` | optional | `[]` (none) | Allow-list of screen names where the button shows. **Only used when `showOnAllScreens` is `false`.** |
| `excludeScreens` | optional | `[]` (none) | Screens the button must **never** show on. Wins over everything. Landing on one also **ends an active call** (see [section 4](#4-widget-visibility)). Use for Login/Splash/checkout. |
| `endCallWhenHiddenByVisibility` | optional | `true` | When `true`, navigating to a screen where the button is hidden ends the live call. Set `false` to let a call continue in the background across such screens. |
| `embedButtonContinuity` | optional | `'continuous'` | `'continuous'` keeps the button mounted across allowed screens (delay applied once). `'perScreen'` re-evaluates and re-delays the button on every screen (card collapses, may flicker). **Neither ends a call** — only the visibility rules / `excludeScreens` do. |
| `embedButtonDelayMs` | optional | `0` | Delay before the button appears on a screen (ms). Use to avoid a flash during screen transitions. |
| `embedButtonVisibilityConfig` | optional | — | Advanced per-group visibility (delay/inset/continuity per set of screens). See [section 7](#7-advanced-integration). |
| `sessionScreenshotEnabled` | optional | `false` | Your consent for in-call screenshots. Capture also requires a backend flag (double consent). Leave off unless RevRag asks. |
| `passiveScreenshotEnabled` | optional | `false` | Your consent for background/cache screenshots. Same double-consent rule. Leave off unless RevRag asks. |
**Minimal, recommended, and full examples** (all assume `useInitialize({ …, appVersion })` ran at the app root — so `appVersion` is not repeated on the provider):
```tsx
// Minimal — button on every screen (navigationRef is still required)
{children}
// Recommended — screen tracking + hide on auth screens
{children}
// Only on specific screens
{children}
```
***
## 3. Screen & Navigation [#3-screen--navigation]
**What:** the SDK needs to know **which screen the user is on** — to decide where the button shows, to give the agent context, and to let the agent navigate. **Why:** every visibility and context decision is keyed on the current screen **name**.
### How screen detection works [#how-screen-detection-works]
Pass your navigation ref as `navigationRef`. The SDK listens to route changes and uses the **active route name** as the current screen. So the names in `includeScreens` / `excludeScreens` must match your **route names exactly**.
```tsx
// → the SDK's screen name for this route is "submit_interest"
```
Name routes with stable, backend-friendly ids (e.g. `submit_interest`, `home_tab`) rather than display titles — they become the agent's screen identity.
### React Navigation (recommended) [#react-navigation-recommended]
Wrap `NavigationContainer` with `EmbedProvider` and pass the shared ref (see [section 1, Step 4](#1-basic-integration)). Nested navigators (tabs inside a stack) work automatically — the SDK tracks the deepest active route.
### Non-React-Navigation / custom / WebView screens [#non-react-navigation--custom--webview-screens]
Screen detection (and therefore widget visibility and UI capture) works **only** through a React Navigation `navigationRef`. A `SCREEN_STATE` event does **not** drive it — it only tags your later analytics events with a screen name:
```tsx
import { Embed, EmbedEventKeys } from '@revrag-ai/embed-react-native';
Embed.Event(EmbedEventKeys.SCREEN_STATE, { screen: 'checkout_webview' }); // analytics tag only
```
If your app uses React Native Navigation (Wix), a state-driven router, or Expo Router, talk to RevRag before integrating — the supported path is real React Navigation routes. WebView content is opaque to the agent; name the wrapping route and exclude it if sensitive.
### Custom screen matching [#custom-screen-matching]
**What:** helps the backend recognize a screen even when its name varies (aliases), by ancestors, or by structure. **When to use:** only if the agent is failing to identify a screen correctly. Off by default — the backend controls the tiers.
```tsx
import { setScreenMatching, SCREEN_MATCHING_ALL } from '@revrag-ai/embed-react-native';
// Turn all matching tiers on (call once at startup)
setScreenMatching(SCREEN_MATCHING_ALL);
// Or opt into specific tiers
setScreenMatching({ aliasMatching: true, ancestorMatching: true });
```
### Visibility props recap [#visibility-props-recap]
Full details are in [section 4](#4-widget-visibility).
* `showOnAllScreens` — button everywhere except `excludeScreens`.
* `includeScreens` — allow-list (used only when `showOnAllScreens` is `false`).
* `excludeScreens` — never-show list; also ends a live call on entry.
* `endCallWhenHiddenByVisibility` — whether a call ends when the button hides.
### When the SDK can't identify a screen [#when-the-sdk-cant-identify-a-screen]
* **Wrong or empty screen name?** Make sure `navigationRef` is the **same ref** passed to `NavigationContainer`, and that `EmbedProvider` wraps the container.
* **Custom/native/WebView screen?** Send `SCREEN_STATE` manually (above).
* **Duplicate/ambiguous names?** Give routes unique names, or enable custom matching.
***
## 4. Widget Visibility [#4-widget-visibility]
### How visibility works [#how-visibility-works]
The button shows based on this precedence:
1. No screen name yet (no `navigationRef`, container not mounted) → **hidden**.
2. `showOnAllScreens` is `true` → **shown**, unless the screen is in `excludeScreens`.
3. Otherwise → shown only if the screen is in `includeScreens` (or a configured group); an empty list means all. **In this mode `excludeScreens` is not consulted** — use `showOnAllScreens` + `excludeScreens` when you need an exclusion list.
Names match **exactly** (case-sensitive) unless you enable `setScreenMatching` tiers.
`embedButtonDelayMs` can delay the appearance; `embedButtonContinuity` decides whether it stays mounted across navigation.
### Show / hide the widget yourself [#show--hide-the-widget-yourself]
You normally don't need to — the provider handles it. For programmatic control of the **expanded card** (not the button itself):
```tsx
import { expandWidget, collapseWidget, isWidgetExpanded } from '@revrag-ai/embed-react-native';
expandWidget(); // open the card — call ONLY when the user asked (counts as a tap)
collapseWidget(); // close the card
isWidgetExpanded(); // → boolean
```
### Why visibility matters (this is important) [#why-visibility-matters-this-is-important]
**Widget visibility is the call's licence.** Two rules follow:
* **`startCall` is refused** on a screen where the button is not shown.
* **Arriving on a hidden screen ends a live call** — unless you set `endCallWhenHiddenByVisibility={false}`.
Visibility also affects what the agent can **do** vs **see**:
* The agent's live in-call UI reading (Action Intelligence) follows the same screens.
**UI capture for the durable cache continues on every screen regardless** — hiding the button hides the *button*, not necessarily all capture. Use `excludeScreens` (not just leaving a screen out of `includeScreens`) for screens that must never be walked or transmitted.
### Troubleshooting [#troubleshooting]
**Widget not appearing.**
Why: not initialized, no `USER_DATA` yet is *not* required for the button to show, but the screen may not be included, or the provider isn't wrapping navigation.
Fix: confirm `useInitialize` ran; confirm the current route name is included (`showOnAllScreens` or in `includeScreens`); confirm `EmbedProvider` wraps `NavigationContainer`; check `embedButtonDelayMs` isn't set too high.
**Widget on the wrong screen.**
Why: route name mismatch between your navigator and `includeScreens`/`excludeScreens`.
Fix: log the active route name and make the lists match it exactly.
**Widget disappears during/after navigation.**
Why: `embedButtonContinuity: 'perScreen'` re-evaluates (and re-delays) on each screen; or the destination isn't included.
Fix: use `'continuous'` (default) to keep it across navigation; ensure the destination screen is included.
**Widget shows on some screens but not others.**
Why: that's `includeScreens`/`excludeScreens` working as configured.
Fix: adjust the lists, or switch to `showOnAllScreens` with a small `excludeScreens`.
***
## 5. Event Capturing [#5-event-capturing]
The SDK captures user activity and forwards it to the agent/analytics. Some events are automatic; some you send yourself.
### The public event channel [#the-public-event-channel]
```tsx
import { Embed, EmbedEventKeys } from '@revrag-ai/embed-react-native';
Embed.Event(EmbedEventKeys.USER_DATA, { app_user_id: 'user_123' }); // identify user (enables calls)
Embed.Event(EmbedEventKeys.SCREEN_STATE, { screen: 'checkout' }); // manual screen report
Embed.Event(EmbedEventKeys.CUSTOM_EVENT, { name: 'coupon_applied' }); // your own event
Embed.Event(EmbedEventKeys.ANALYTICS_DATA, { event_name: 'cta_click', metadata: {} });
```
`Embed.Event` never throws; pass a third callback to observe delivery. Use `Embed.on` / `Embed.off` for local listeners.
### Automatic click / press tracking [#automatic-click--press-tracking]
**What:** the SDK patches React Native touchables so taps on your app become `ANALYTICS_DATA` events.
**Supported components:** `Pressable`, `TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback`, and `TouchableNativeFeedback` (Android). **When it emits:** only while the widget is visible on the current screen, and only if click tracking is enabled on the backend (`action_config.flags.clickTracking`).
**The event name says what the tap did** (same rules as the Android and web SDKs). The touchable's `role` / `accessibilityRole` decides it:
| Touchable's role | `event_name` | `data.type` |
| ------------------------------------ | ----------------- | ----------------- |
| `button`, `imagebutton`, `tab` | `_pressed` | `button_click` |
| `link`, or no role | `_pressed` | `element_click` |
| `checkbox`, `switch`, `togglebutton` | `_toggled` | `checkbox_toggle` |
| `radio` | `_selected` | `radio_select` |
| `combobox` | `_opened` | `button_click` |
**`` is the first of these that exists:**
1. Your own identifier, used as written: the `id`, `name` or `testID` prop. An existing `submit_pressed` stays `submit_pressed`.
2. The `accessibilityLabel` / `aria-label`, then the visible child text, cleaned up: lowercase, at most four words, words with digits and currency words left out. "Pay ₹5,000 now" becomes `pay_now`.
3. A child icon's name or image file name, for icon-only buttons: `` → `arrow_back`.
4. The role, then the component (`pressable`).
Text that contains an email, UPI id or masked number (`XXXX 1234`) is never used, and nothing under `nativeID="embed-redact…"` is read. Text drawn inside your own custom component is out of reach — give such buttons an `id`.
Example:
```tsx
Submit
```
This emits an `ANALYTICS_DATA` event named **`submit_pressed`**, with
`data: { element_id: 'submit', type: 'element_click', kind: 'element', source: 'user', click_sequence: '12', component: 'Pressable' }`.
`role` and `checked` are added when known, and `legacy_event_name` when the name differs from what 1.1 sent.
**Toggles:** declare the role and state on the touchable, and the event carries the state **after** the tap:
```tsx
setDarkMode((v) => !v)}>
Dark mode
// → "dark_mode_toggled", data.checked: "true"
```
Not reported: the SDK widget's own buttons, the agent's own taps, and a second identical event within 300 ms. React Native's ``, `