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
- Basic Integration — install, native setup, provider, first call
- Configuration —
useInitializeandEmbedProvideroptions - Screen & Navigation — how the SDK knows where the user is
- Widget Visibility — where the button shows, and why it matters
- Event Capturing — data events, click tracking, agent lifecycle
- Action Intelligence / UI Capture — how the agent sees and acts
- Advanced Integration — manual control, groups, permissions
- Troubleshooting — problem → why → fix
- Debugging Guide — rule out one layer at a time
- Integration Examples — copy-paste patterns
- 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-assetWhy 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() 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
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
| 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
| 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). 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. |
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):
// 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.
React Navigation (recommended)
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 onlyIf 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 exceptexcludeScreens.includeScreens— allow-list (used only whenshowOnAllScreensisfalse).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
navigationRefis the same ref passed toNavigationContainer, and thatEmbedProviderwraps the container. - Custom/native/WebView screen? Send
SCREEN_STATEmanually (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:
- No screen name yet (no
navigationRef, container not mounted) → hidden. showOnAllScreensistrue→ shown, unless the screen is inexcludeScreens.- Otherwise → shown only if the screen is in
includeScreens(or a configured group); an empty list means all. In this modeexcludeScreensis not consulted — useshowOnAllScreens+excludeScreenswhen 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(); // → booleanWhy visibility matters (this is important)
Widget visibility is the call's licence. Two rules follow:
startCallis 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.
Navigation, screen change, scroll, UI interactions
- Screen changes are captured automatically when you pass
navigationRef(or via manualSCREEN_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 value | Fires when |
|---|---|
AGENT_VISIBLE | The floating button appears on the current screen. |
AGENT_TAP_TO_OPEN | The user expands the widget card. |
AGENT_TAP_TO_CLOSE | The user collapses the widget card. |
POPUP_MESSAGE_VISIBLE | An inactivity tooltip / nudge is shown. |
AGENT_CONVERSATION_STARTED | A voice call connects. |
AGENT_CONVERSATION_ENDED | A voice call ends (any reason). |
GEN_TOOL_TRIGGERED | The agent invokes an AI tool mid-call. |
MICROPHONE_PERMISSION_ALLOWED | Mic permission was granted. |
MICROPHONE_PERMISSION_DENIED | Mic 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 elementId | Action-Intelligence stableId | |
|---|---|---|
| Purpose | Click analytics event name | Identify a node in the UI tree for the agent |
| Derived from | id → name → generated | testID → id/nativeID → visible-text → role |
| Example | submit → submit_pressed | button_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: testID → id/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
secureTextEntryon theTextInput. 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
- Pass
navigationRefso screen context is correct. - Add
testIDs to important interactive elements. - Mark sensitive fields (
secureTextEntry/embed-redact). - Use
excludeScreensfor 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 button | endCallWhenHiddenByVisibility={false} |
| Different button delay/position per group of screens | embedButtonVisibilityConfig |
| The agent to recognize a hard-to-identify screen | setScreenMatching |
| Reliable agent targeting on custom components | testIDs + nativeID |
| Your own conversation UI (agent shouldn't hold the mic) | pauseAgent / resumeAgent |
| Drive calls from your own buttons | startCall / endCall |
| Report screens without React Navigation | SCREEN_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 backSession / 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 userPermissions (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 settingsScreenshots (opt-in, double consent)
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
MainApplicationsetup (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:
NSMicrophoneUsageDescriptionis mandatory; runnpx react-native-assetonce for fonts;pod installafter 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.
- SDK initialized? Confirm
useInitializeran with a validapiKey/embedUrl(its returnederrorisnull). - App version set? Confirm
appVersioninuseInitializeor the provider (no warning in logs). - Widget config loaded? After init, the widget config should arrive; a persistent "no config" points to a bad
apiKey/embedUrlor network. - Screen detected? Log the active route name; confirm it's what your include/exclude lists expect.
- Widget visible? Confirm the current screen passes the visibility rules (section 4).
- Events captured? Tap a named button; confirm an
ANALYTICS_DATA<id>_pressedevent and correct screen. - UI captured? Start a call; in dev the SDK logs snapshot sends (
[Embed Snapshot] SENT screen=… nodes/bytes). - Logs. Development logs are
__DEV__-gated and prefixed[Embed …]. Watchadb logcat(Android) / Xcode console (iOS) filtered toEmbed. - Network/API. Confirm the device reaches your
embedUrl; check for auth (401/403) or connectivity failures. - 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 testID5. 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 anid/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 smallexcludeScreens. AddincludeScreens, groups, or custom matching only when a real problem appears. - Protect sensitive data.
secureTextEntryfor passwords/OTPs;embed-redactfor other sensitive inputs. Never rely on the field "looking" sensitive. - Always set
appVersion(viauseInitialize) 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 |
| npm | https://www.npmjs.com/package/@revrag-ai/embed-react-native |
| React Native | 0.76 – 0.86 (reference: 0.78) · Hermes |
| Versions & pitfalls | Dependency & Compatibility Guide |
| Release notes | React Native SDK release notes |
Support
- Docs: https://docs.revrag.ai
- Email: contact@revrag.ai
- Dashboard: app.revrag.ai