# 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 NSMicrophoneUsageDescription This 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 ``, `