View as Markdownllms.txtRelease Notes

React Native Integration Guide

The complete guide for adding the RevRag embedded voice agent ("AI Saathi") to a React Native app. It goes basic → intermediate → advanced, and every feature is explained as: What is it? → Why do I need it? → How do I configure it? → Example → Common problem → How to fix it.

Package: @revrag-ai/embed-react-native · SDK 1.1.0 on npm · React Native 0.76 – 0.86 (reference: 0.78, Hermes)

You do not need to know how the SDK works internally to follow this. If you only read one section, read 1. Basic Integration — that alone gets a working widget on screen.

Get your API key from https://app.revrag.ai.


Table of contents

  1. Basic Integration — install, native setup, provider, first call
  2. ConfigurationuseInitialize and EmbedProvider options
  3. Screen & Navigation — how the SDK knows where the user is
  4. Widget Visibility — where the button shows, and why it matters
  5. Event Capturing — data events, click tracking, agent lifecycle
  6. Action Intelligence / UI Capture — how the agent sees and acts
  7. Advanced Integration — manual control, groups, permissions
  8. Troubleshooting — problem → why → fix
  9. Debugging Guide — rule out one layer at a time
  10. Integration Examples — copy-paste patterns
  11. Best Practices

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

# The SDK
npm install @revrag-ai/embed-react-native

# 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.

Step 2 — Native setup (required, one time)

Babel — add the reanimated plugin as the last plugin in babel.config.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/<your/package>/MainApplication.kt:

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/<YourApp>/AppDelegate.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/<YourApp>/Info.plist (missing this crashes the app when a call starts):

<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access to talk to the AI agent.</string>

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: <uses-permission android:name="android.permission.CAMERA" tools:node="remove"/>.

Do not call LiveKit's registerGlobals() yourselfuseInitialize 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

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.

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

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).

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 (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        {/* appVersion lives in useInitialize (required); no need to repeat it here */}
        <EmbedProvider
          navigationRef={navigationRef}
          showOnAllScreens
          excludeScreens={['Splash', 'Login']}
        >
          <NavigationContainer ref={navigationRef}>
            {/* your navigator */}
          </NavigationContainer>
        </EmbedProvider>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

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)

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.

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

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 (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        <EmbedProvider
          navigationRef={navigationRef}
          showOnAllScreens
          excludeScreens={['Login']}
        >
          <NavigationContainer ref={navigationRef}>
            <Stack.Navigator>
              <Stack.Screen name="Login" component={LoginScreen} />
              <Stack.Screen name="Home" component={HomeScreen} />
            </Stack.Navigator>
          </NavigationContainer>
        </EmbedProvider>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

That's the whole minimum integration. Everything below is optional refinement.


2. Configuration

Two places hold configuration: useInitialize(...) (credentials) and <EmbedProvider ...> (behavior). Backend-controlled settings (agent voice, colors, feature flags) are managed by RevRag remotely — you do not set those in code.

useInitialize options

OptionRequiredDefaultWhat it does / when to use
apiKeyYour RevRag API key. Identifies your tenant.
embedUrloptionalRevRag defaultYour RevRag server URL. Set it if RevRag gave you a specific host.
appVersionYour 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

PropRequiredDefaultWhat it does / when to use
childrenYour app tree (must contain the NavigationContainer).
appVersionoptionalOverride 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.
showOnAllScreensoptionalfalseShow the button on every screen except excludeScreens. The simplest setup.
includeScreensoptional[] (none)Allow-list of screen names where the button shows. Only used when showOnAllScreens is false.
excludeScreensoptional[] (none)Screens the button must never show on. Wins over everything. Landing on one also ends an active call (see section 4). Use for Login/Splash/checkout.
endCallWhenHiddenByVisibilityoptionaltrueWhen 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.
embedButtonContinuityoptional'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.
embedButtonDelayMsoptional0Delay before the button appears on a screen (ms). Use to avoid a flash during screen transitions.
embedButtonVisibilityConfigoptionalAdvanced per-group visibility (delay/inset/continuity per set of screens). See section 7.
sessionScreenshotEnabledoptionalfalseYour consent for in-call screenshots. Capture also requires a backend flag (double consent). Leave off unless RevRag asks.
passiveScreenshotEnabledoptionalfalseYour 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):

// Minimal — button on every screen (navigationRef is still required)
<EmbedProvider navigationRef={navigationRef} showOnAllScreens>{children}</EmbedProvider>

// Recommended — screen tracking + hide on auth screens
<EmbedProvider
  navigationRef={navigationRef}
  showOnAllScreens
  excludeScreens={['Splash', 'Login', 'OtpVerify']}
>{children}</EmbedProvider>

// Only on specific screens
<EmbedProvider
  navigationRef={navigationRef}
  includeScreens={['Home', 'LoanCalculator', 'SubmitInterest']}
>{children}</EmbedProvider>

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

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.

<Stack.Screen name="submit_interest" component={ConfirmInterestScreen} />
// → 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.

Wrap NavigationContainer with EmbedProvider and pass the shared ref (see section 1, Step 4). Nested navigators (tabs inside a stack) work automatically — the SDK tracks the deepest active route.

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:

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

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.

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 (full details in section 4)

  • 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

  • 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

How visibility works

The button shows based on this precedence:

  1. No screen name yet (no navigationRef, container not mounted) → hidden.
  2. showOnAllScreens is trueshown, 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

You normally don't need to — the provider handles it. For programmatic control of the expanded card (not the button itself):

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)

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

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

The SDK captures user activity and forwards it to the agent/analytics. Some events are automatic; some you send yourself.

The public event channel

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

What: the SDK patches React Native touchables so taps become analytics 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.

How the element id is chosen (this determines your analytics event name):

id prop  →  name prop  →  generated fallback  (e.g. "touchableopacity-7")

testID and accessibilityLabel are not used for the click id. If a button has neither id nor name, you get a generated id like pressable-3, and the analytics event is not human-readable.

Example:

<Pressable name="submit">
  <Text>Submit</Text>
</Pressable>

This emits an ANALYTICS_DATA event named submit_pressed, with metadata { timestamp, type: 'press', component: 'Pressable', clickSequence }.

To get meaningful analytics, always put an id or name on interactive elements.

  • Screen changes are captured automatically when you pass navigationRef (or via manual SCREEN_STATE).
  • Scroll and general UI interaction are captured internally to keep the agent's view fresh during a call (they feed UI capture, section 6) — you don't wire these up.
  • Manual actions — call control (startCall/endCall), widget control, and custom events are the actions you trigger yourself.

Agent lifecycle events (listen to the agent)

What: one subscription that receives every SDK lifecycle event — the same model as Android's SdkLifecycleListener. A single embedOnAgent handler is called for all events; you branch on event.type. embedOnAgent returns a handle string; pass it to embedOffAgent to unsubscribe.

The complete list of events you can listen to:

AgentEvent valueFires when
AGENT_VISIBLEThe floating button appears on the current screen.
AGENT_TAP_TO_OPENThe user expands the widget card.
AGENT_TAP_TO_CLOSEThe user collapses the widget card.
POPUP_MESSAGE_VISIBLEAn inactivity tooltip / nudge is shown.
AGENT_CONVERSATION_STARTEDA voice call connects.
AGENT_CONVERSATION_ENDEDA voice call ends (any reason).
GEN_TOOL_TRIGGEREDThe agent invokes an AI tool mid-call.
MICROPHONE_PERMISSION_ALLOWEDMic permission was granted.
MICROPHONE_PERMISSION_DENIEDMic permission was denied/blocked.

AGENT_CONNECTED / AGENT_DISCONNECTED also exist but are deprecated aliases of AGENT_CONVERSATION_STARTED / AGENT_CONVERSATION_ENDED — use the newer names.

Full example — handle every event with one listener:

import React, { useEffect } from 'react';
import { embedOnAgent, embedOffAgent, AgentEvent } from '@revrag-ai/embed-react-native';

function useAgentLifecycle() {
  useEffect(() => {
    const handle = embedOnAgent((event) => {
      // `event.type` is the AgentEvent; `event.timestamp` and event-specific
      // fields (e.g. event.metadata) ride along on the same object.
      switch (event.type) {
        case AgentEvent.AGENT_VISIBLE:
          console.log('Button visible on screen');
          break;
        case AgentEvent.AGENT_TAP_TO_OPEN:
          console.log('User opened the widget');
          break;
        case AgentEvent.AGENT_TAP_TO_CLOSE:
          console.log('User closed the widget');
          break;
        case AgentEvent.POPUP_MESSAGE_VISIBLE:
          console.log('Nudge shown, trigger:', event.metadata?.trigger);
          break;
        case AgentEvent.AGENT_CONVERSATION_STARTED:
          console.log('Call started at', event.timestamp);
          break;
        case AgentEvent.AGENT_CONVERSATION_ENDED:
          console.log('Call ended, duration:', event.metadata?.callDuration);
          break;
        case AgentEvent.GEN_TOOL_TRIGGERED:
          console.log('Agent used a tool mid-call');
          break;
        case AgentEvent.MICROPHONE_PERMISSION_ALLOWED:
          console.log('Mic granted');
          break;
        case AgentEvent.MICROPHONE_PERMISSION_DENIED:
          console.warn('Mic denied:', event.error_message);
          break;
        default:
          break;
      }
    });

    // ALWAYS unsubscribe on unmount, or handlers leak.
    return () => embedOffAgent(handle);
  }, []);
}

Analytics helpers

import { trackEmbedEvent, trackFormEvent, trackRageClick } from '@revrag-ai/embed-react-native';
trackEmbedEvent('promo_seen', { id: 'diwali' });

Two ids that are easy to confuse

Click-event elementIdAction-Intelligence stableId
PurposeClick analytics event nameIdentify a node in the UI tree for the agent
Derived fromidname → generatedtestIDid/nativeID → visible-text → role
Examplesubmitsubmit_pressedbutton_submit_interest

They are independent. The same button can be submit in click analytics and button_submit_interest in the UI tree. Setting testID improves the stableId; setting id/name improves the click elementId. For the cleanest result, set a testID and an id/name with the same value.


6. Action Intelligence / UI Capture

What it is: the agent can see your screen and act on it (tap, type, scroll, highlight) to help the user complete a task. Why you rarely configure it: it works out of the box; your only real job is to name elements well (section 11) so the agent targets the right thing.

The flow, in plain terms

Your app UI
  → Capture        (the SDK reads the on-screen elements)
  → UI Snapshot    (a clean, structured tree of those elements)
  → Backend        (sent live during a call, and cached durably over HTTP)
  → Action Intelligence  (the agent decides what to do)
  → Action         (tap / type / select / scroll / highlight on your UI)
  → Verification   (the SDK checks the action actually changed the screen)

What is captured, and when

  • What: interactive and meaningful elements — buttons, inputs, text, form fields, checkboxes/radios/switches/sliders — with their on-screen position and a stable id. Purely decorative wrapper views are dropped automatically.
  • When: on connect (a baseline), and then on navigation, scroll, taps, and when the user finishes speaking. Identical screens are de-duplicated (nothing re-sent if nothing changed).
  • Order: elements are sent top-to-bottom, matching the screen, with a structure map so the backend can rebuild the exact hierarchy.

Two channels (you don't manage these — just know they exist)

  • Live (in-call): a size-capped snapshot of the current screen, sent over the call's data channel for real-time planning.
  • Durable cache (HTTP): a persistent map of screens/elements/navigation built up over time, synced in the background.

How elements are identified

The agent targets elements by stableId: testIDid/nativeID → visible text → role. The single most useful thing you can do is add testIDs to the elements you want the agent to operate reliably.

<TextInput testID="pan_card_number" value={pan} onChangeText={setPan} />
<TouchableOpacity testID="button_submit"><Text>Submit</Text></TouchableOpacity>

Sensitive fields (do this)

  • Passwords/OTPs: use secureTextEntry on the TextInput. The SDK never captures the value, and the agent refuses to read or type into it.
  • Other sensitive fields (masking capture but still lettable): add nativeID="embed-redact-<anything>". The value/text is never captured, but the agent may still type into it.

What you need to do on the host side

  1. Pass navigationRef so screen context is correct.
  2. Add testIDs to important interactive elements.
  3. Mark sensitive fields (secureTextEntry / embed-redact).
  4. Use excludeScreens for screens that must never be captured/transmitted.

Common problem

Agent taps the wrong element / can't find a field. Why: the element has no stable id, so it falls back to text/role and collides with another node. Fix: add a unique testID. For dynamic lists, make the testID unique per row.


7. Advanced Integration

Most apps never need this section. Reach for it only when the basics don't cover a real requirement — over-configuring causes more bugs than it prevents.

When advanced config is actually necessary

You need…Use
Agent to keep running while the user visits a screen without the buttonendCallWhenHiddenByVisibility={false}
Different button delay/position per group of screensembedButtonVisibilityConfig
The agent to recognize a hard-to-identify screensetScreenMatching
Reliable agent targeting on custom componentstestIDs + nativeID
Your own conversation UI (agent shouldn't hold the mic)pauseAgent / resumeAgent
Drive calls from your own buttonsstartCall / endCall
Report screens without React NavigationSCREEN_STATE events

Custom per-group visibility

<EmbedProvider
  showOnAllScreens
  embedButtonVisibilityConfig={{
    defaultDelayMs: 300,
    groups: [
      { id: 'loan_flow', screens: ['LoanCalc', 'SubmitInterest'],
        continuity: 'continuous', delayMs: 0 },
    ],
  }}
>{children}</EmbedProvider>

Full prop tables, JSON examples, and behavior notes for grouped visibility live in the companion guide: EmbedProvider advanced.

Manual call control

import {
  startCall, endCall, isCallActive, getAgentTriggerMode,
} from '@revrag-ai/embed-react-native';

const started = await startCall();      // returns false if refused (no widget on screen, no user set)
if (isCallActive()) await endCall();

startCall() runs permission → token → connect. It returns false when no button is shown on the current screen (or none is mounted yet). If USER_DATA was never sent the token request aborts but the call still resolves true — so observe AGENT_CONVERSATION_STARTED / isCallActive() for the real outcome.

Pause the agent without ending the call

import { pauseAgent, resumeAgent, isAgentPaused } from '@revrag-ai/embed-react-native';
await pauseAgent();   // agent stops listening/speaking; room stays up
await resumeAgent();  // give the mic back

Session / user switch

import { EmbedLogout } from '@revrag-ai/embed-react-native';
await EmbedLogout();  // ends call, wipes on-device cache, forgets the user
// then send a fresh USER_DATA for the next user

Permissions (manual)

import { checkPermissions, openAppSettings, MIC_PERMISSION_REQUIRED_MESSAGE }
  from '@revrag-ai/embed-react-native';
const result = await checkPermissions();   // 'granted' | 'denied' | 'blocked'; never throws
if (result === 'blocked') openAppSettings(); // user must re-enable mic in OS settings

Both sessionScreenshotEnabled and passiveScreenshotEnabled default false and also require a backend flag. Leave off unless RevRag explicitly enables the feature for you.

Platform / version notes

  • React Native: the tested reference is 0.78 with the New Architecture and Hermes; 0.76 – 0.86 are supported with the right dependency stack (RN ≥ 0.82 needs Reanimated 4 + react-native-worklets + Gesture Handler 3). Version pins per RN line: Dependency & Compatibility Guide.
  • Android: add the LiveKit MainApplication setup (section 1); if using ProGuard, keep the Embed classes (see below) — WebRTC/LiveKit/Lottie ship their own consumer keep rules. LiveKit increases native binary size — for production APK size, see Android app size optimization.
  • iOS: NSMicrophoneUsageDescription is mandatory; run npx react-native-asset once for fonts; pod install after every dependency change.

ProGuard (android/app/proguard-rules.pro):

-keep class com.revragai.embedreactnative.** { *; }
# WebRTC / LiveKit / Lottie ship their own consumer keep rules — nothing else needed.

8. Troubleshooting

Format for each: Problem → Why → Fix → Example.

Crashes

audioRecordSamplesDispatcher is not initialized! on first call. Why: LiveKit native setup missing. Fix: add LiveKitReactNative.setup(...) to MainApplication.kt and AppDelegate.swift, then rebuild. Example: see section 1, Step 2.

iOS crash: "attempted to access privacy-sensitive data without a usage description". Why: NSMicrophoneUsageDescription missing from Info.plist. Fix: add the key and rebuild.

Reanimated errors / white screen after install. Why: the reanimated Babel plugin is missing or not last. Fix: put 'react-native-reanimated/plugin' last in babel.config.js, then npx react-native start --reset-cache and rebuild.

Native dependency / version conflict at build time. Why: a peer dependency is missing or mismatched. Fix: install all peer deps from section 1; check every version against the Dependency & Compatibility Guide; on iOS run pod install; clean builds (gradlew clean, delete Pods/).

When reporting a crash, include: the full stack trace, platform + OS version, RN version, SDK version (@revrag-ai/embed-react-native from package.json), the screen it happened on, and whether it was during install, a call, or navigation.

Widget visibility

Not visible. Why: useInitialize didn't run, screen not included, or provider not wrapping navigation. Fix: verify all three (see section 4).

On the wrong screen. Why: route-name mismatch. Fix: match includeScreens/excludeScreens to actual route names.

Disappears during navigation. Why: embedButtonContinuity: 'perScreen' or destination not included. Fix: use 'continuous'; include the destination.

Not back after returning to a screen. Why: a delay (embedButtonDelayMs) re-applies, or perScreen continuity re-evaluates. Fix: lower the delay; use 'continuous'.

Screen matching

Wrong screen detected / name mismatch. Why: display title used instead of a stable route name, or duplicate names. Fix: give routes unique, stable names; report custom screens via SCREEN_STATE.

Screen still not recognized by the agent. Why: the backend can't map it. Fix: setScreenMatching(SCREEN_MATCHING_ALL) at startup, or enable specific tiers.

Events

Click events not captured. Why: widget not visible on that screen, or backend click tracking disabled. Fix: ensure the button shows there; confirm the flag with RevRag.

Generated ids (pressable-4) instead of meaningful ones. Why: the element has no id/name. Fix: add id or name (see section 5).

Events on the wrong screen. Why: screen name mismatch/stale. Fix: pass navigationRef or send SCREEN_STATE.

Agent not receiving UI information. Why: not in a call, or the screen is excluded. Fix: start a call; check the screen isn't in excludeScreens.

UI capture

Tree missing elements. Why: decorative wrappers are pruned by design; or the element mounts late. Fix: only interactive/meaningful nodes are expected; add a testID to force importance.

Wrong element ids. Why: no testID, so text/role is used and collides. Fix: add unique testIDs.

Stale tree / capture too early. Why: captured mid-transition. Fix: the SDK re-captures on settle; if you drive navigation manually, send SCREEN_STATE after the screen renders.

Dialog/overlay not detected. Why: it's a separate native window. Fix: usually handled; if not, ensure the overlay is a normal React tree under the provider.

Sensitive fields captured. Why: not marked. Fix: secureTextEntry or nativeID="embed-redact-…" (section 6).

Call / widget

Call won't start. Why: no USER_DATA sent, or no button on the current screen, or mic permission denied. Fix: send USER_DATA; start from an included screen; grant mic.

Call ends unexpectedly on navigation. Why: moved to a hidden/excluded screen with endCallWhenHiddenByVisibility (default true). Fix: include the screen, or set the prop to false.

Audio / caption issues. Why: mic permission, or LiveKit native setup. Fix: verify permission and LiveKitReactNative.setup(); test on a real device (emulators have flaky audio).


9. Debugging Guide

Work top-down; each step rules out a layer.

  1. SDK initialized? Confirm useInitialize ran with a valid apiKey/embedUrl (its returned error is null).
  2. App version set? Confirm appVersion in useInitialize or the provider (no warning in logs).
  3. Widget config loaded? After init, the widget config should arrive; a persistent "no config" points to a bad apiKey/embedUrl or network.
  4. Screen detected? Log the active route name; confirm it's what your include/exclude lists expect.
  5. Widget visible? Confirm the current screen passes the visibility rules (section 4).
  6. Events captured? Tap a named button; confirm an ANALYTICS_DATA <id>_pressed event and correct screen.
  7. UI captured? Start a call; in dev the SDK logs snapshot sends ([Embed Snapshot] SENT screen=… nodes/bytes).
  8. Logs. Development logs are __DEV__-gated and prefixed [Embed …]. Watch adb logcat (Android) / Xcode console (iOS) filtered to Embed.
  9. Network/API. Confirm the device reaches your embedUrl; check for auth (401/403) or connectivity failures.
  10. Collect info. For a bug report: platform + OS, RN version, SDK version, screen, repro steps, and logs from steps 7–9.

10. Integration Examples

All examples assume useInitialize({ apiKey, appVersion }) already ran at the app root (see section 1), so appVersion is not repeated on <EmbedProvider>.

1. Basic React Native app (button everywhere)

const navigationRef = useRef(null);   // required — the SDK reads the screen from it
<GestureHandlerRootView style={{ flex: 1 }}>
  <SafeAreaProvider>
    <EmbedProvider navigationRef={navigationRef} showOnAllScreens>
      <NavigationContainer ref={navigationRef}>{/* your navigator */}</NavigationContainer>
    </EmbedProvider>
  </SafeAreaProvider>
</GestureHandlerRootView>

2. React Navigation app (screen tracking)

const navigationRef = useRef(null);
<EmbedProvider navigationRef={navigationRef} showOnAllScreens excludeScreens={['Login']}>
  <NavigationContainer ref={navigationRef}>{/* navigator */}</NavigationContainer>
</EmbedProvider>

3. App using Pressable (meaningful click ids)

<Pressable name="apply_loan" onPress={apply}><Text>Apply</Text></Pressable>
// → analytics event "apply_loan_pressed"

4. App using TouchableOpacity

<TouchableOpacity id="submit_interest" testID="button_submit_interest" onPress={submit}>
  <Text>Submit Interest</Text>
</TouchableOpacity>
// click id = "submit_interest"; agent stableId anchors on testID

5. App with custom screen matching

useEffect(() => { setScreenMatching(SCREEN_MATCHING_ALL); }, []);

6. App using Action Intelligence (naming for the agent)

<TextInput testID="full_name" value={name} onChangeText={setName} />
<TextInput testID="pan" secureTextEntry value={pan} onChangeText={setPan} />
<TouchableOpacity testID="button_submit"><Text>Submit</Text></TouchableOpacity>

7. App requiring manual visibility / call control

const start = async () => { if (!(await startCall())) alert('Cannot start here'); };
<Button title="Talk to us" onPress={start} />

8. App with advanced event handling

useEffect(() => {
  const h = embedOnAgent((e) => {
    if (e.type === AgentEvent.AGENT_CONVERSATION_ENDED) refreshUi();
  });
  return () => embedOffAgent(h);
}, []);

11. Best Practices

  • Name interactive elements. Put a testID (for the agent) and an id/name (for click analytics) on every button/input that matters. Ideally the same value for both.
  • Use stable route names (submit_interest, not "Submit Interest!") — they become the agent's screen identity and your include/exclude keys.
  • Pass navigationRef. It's the difference between automatic, correct screen context and manual bookkeeping.
  • Don't over-configure. Start with showOnAllScreens + a small excludeScreens. Add includeScreens, groups, or custom matching only when a real problem appears.
  • Protect sensitive data. secureTextEntry for passwords/OTPs; embed-redact for other sensitive inputs. Never rely on the field "looking" sensitive.
  • Always set appVersion (via useInitialize) so your analytics aren't attributed to "unknown".
  • Test calls on real devices. Audio/WebRTC is unreliable on emulators/simulators.
  • Debug top-down using section 9 before filing a bug; include the info list from section 8.

Reference

Package@revrag-ai/embed-react-native
npmhttps://www.npmjs.com/package/@revrag-ai/embed-react-native
React Native0.76 – 0.86 (reference: 0.78) · Hermes
Versions & pitfallsDependency & Compatibility Guide
Release notesReact Native SDK release notes

Support