View as Markdownllms.txt

RevRag React Native SDK — AI Agent Integration Master Prompt

How to use: paste this entire document into your AI coding agent (Copilot, Cursor, Claude Code, etc.) inside the React Native app you want to integrate. The agent will inspect your app, install and configure @revrag-ai/embed-react-native, ask you the few decisions only you can make, then validate the integration end-to-end and report.

Every rule below is derived from the SDK's source (v1.1.0). Where this prompt and a generic RN tutorial disagree, this prompt is correct for this SDK.


You are an expert React Native integration agent. Take this application from zero → working → validated RevRag SDK integration with minimal manual work.

Prime directives

  1. Inspect first, modify second. Never assume RN version, architecture, navigation library, package manager, or native setup. Read the project.
  2. Preserve host behavior. Merge into existing config; never overwrite babel.config, metro.config, MainApplication, AppDelegate, or the root component blindly.
  3. Never claim success without running the validation in §16–§21 and observing the log lines named there.
  4. Ask the developer for the decisions in §13 — do not guess them.
  5. Never hide a crash or error. Report SDK-side issues you cannot fix.
  6. Do not add dependencies, permissions, or config the SDK does not require.

1. Inspect the existing application

Record each of these before changing anything:

WhatHow to find it
RN / React versionpackage.json; node -p "require('react-native/package.json').version"
New Architecture / Hermesandroid/gradle.properties (newArchEnabled, hermesEnabled); ios/Podfile (RCT_NEW_ARCH_ENABLED, hermes_enabled)
Android toolchainandroid/build.gradle ext { minSdkVersion, compileSdkVersion, targetSdkVersion, kotlinVersion, ndkVersion }, AGP/Gradle versions
iOS deployment targetios/Podfile platform :ios, Xcode project
Package managerpackage-lock.json / yarn.lock (+ .yarnrc.yml → berry) / pnpm-lock.yaml (+ .npmrc)
Navigation@react-navigation/*, expo-router (+ app/ dir), react-native-navigation (Wix), or none / state-driven
Where NavigationContainer mountsis it conditional (auth gate, useFonts, splash)? does a navigationRef already exist?
Root component / entryindex.js (AppRegistry.registerComponent), root App
Babelbabel.config.js — existing presets/plugins (nativewind, module-resolver, dotenv, env/overrides branches)
Existing Reanimated / worklets / gesture-handler / lottie / linear-gradient / safe-area / async-storageversions in package.json
Existing LiveKit / WebRTC@livekit/*, react-native-webrtc; grep -rn "registerGlobals|LivekitReactNative.setup|LiveKitReactNative.setup" index.js android ios
Providers above the rootGestureHandlerRootView, SafeAreaProvider, PaperProvider, PortalProvider, BottomSheetModalProvider — record nesting order
Permissions already declaredAndroidManifest.xml, Info.plist
Env/config system.env, react-native-config, expo-constants, custom
App version sourcepackage.json version, android/app/build.gradle versionName, Info.plist, or an env value
Expo?expo in deps; no android//ios/ dirs ⇒ managed workflow

2. Determine compatibility

Supported: React Native 0.76 – 0.86, Hermes (required), JDK 17, AGP ≥ 8.6, Android minSdkVersion ≥ 24 (hard-coded by LiveKit's WebRTC — non-negotiable), iOS 15.1 (16.0 on RN ≥ 0.81). Verified: native build + on-device runtime on RN 0.78 and 0.86 (Android). Below 0.76: unsupported (the SDK ships RN-0.78 codegen).

Two known-good stacks — pick by RN line and pin exactly:

PackageStack A: RN 0.78–0.81Stack B: RN 0.82–0.86 (New-Arch only)
react19.0.0 (18.3.1 on 0.76/0.77)must satisfy RN's peer (0.86 → ^19.2.3)
@livekit/react-native2.11.1 (min 2.10.2)2.11.1
@livekit/react-native-webrtc144.1.1 (min 137.0.0)144.1.1
react-native-reanimated3.19.54.5.2
react-native-worklets— (do NOT install with RA3)0.11.0
react-native-gesture-handlerlatest 2.x (≥ 2.18.0)3.0.2
react-native-safe-area-context5.8.05.8.0
@react-native-async-storage/async-storage2.2.02.2.0
lottie-react-native7.3.87.3.8
react-native-linear-gradient2.8.32.8.3

If the app is on RN ≥ 0.82 it must be on Reanimated 4 + worklets + Gesture Handler 3 (Reanimated 3 / GH 2 cannot run there). If on RN ≤ 0.81 with Reanimated 4, keep it (New Arch only). Do not upgrade/downgrade unrelated dependencies. If an existing dependency is incompatible: explain, pick the compatible version from the table, update it and its native config, re-validate.


3. Install the SDK

npm install @revrag-ai/embed-react-native@^1.1.0

If given a local tarball, install that path instead. After install, verify the package is complete — v1.1.0 shipped without native sources:

ls node_modules/@revrag-ai/embed-react-native/android/src node_modules/@revrag-ai/embed-react-native/ios/*.m

Both must exist. Autolinking then links the module; no manual package registration.

Never add livekit-client to the app's package.json — the SDK bundles it. A host copy produces two runtimes and silently breaks calls (§4).


4. Install and configure dependencies

Install every peer from §2 at the pinned version. Then verify:

npm ls livekit-client --all                  # MUST be exactly one path
npm ls @livekit/react-native-webrtc --all    # exactly one
npm ls @livekit/react-native @livekit/react-native-webrtc   # versions match a released pair
npm ls react-native-reanimated react-native-worklets react-native-gesture-handler
npm ls react react-native                   # exactly one copy each (monorepo/pnpm hoisting!)

Expected noise (harmless): every install prints npm warn ERESOLVE overriding peer dependency and npm ls may mark React invalid — a transitive LiveKit web helper peers react-dom. Do not "fix" it; do not gate CI on npm ls exit code.

Package-manager rules

  • yarn classic: does not auto-install peers — install each explicitly; check ls node_modules/@livekit/react-native.
  • yarn berry: requires nodeLinker: node-modules in .yarnrc.yml (PnP breaks LiveKit resolution).
  • pnpm: auto-install-peers turns every * peer into latest (can pull RA4/GH3 onto RN < 0.82) — pin explicitly; do not enable strict-peer-dependencies.
  • Expo: managed workflow needs a development build (Expo Go cannot run it) plus @livekit/react-native-expo-plugin and @config-plugins/react-native-webrtc; expo-doctor's "unsupported on New Architecture" warning for LiveKit is a false positive.

Reanimated / worklets (Babel)

Add the plugin as the last entry of the top-level plugins array — and of every env/overrides branch that defines plugins — without removing existing presets or plugins (nativewind/babel, module-resolver, dotenv, babel-preset-expo, …):

  • Reanimated 3: 'react-native-reanimated/plugin'
  • Reanimated 4: 'react-native-worklets/plugin' (never both; never worklets with RA3)

Then npx react-native start --reset-cache. A missing/misordered plugin does not crash; the SDK degrades to no-op animations and logs [Embed SDK] react-native-reanimated ….

Gesture Handler

The root must be wrapped in <GestureHandlerRootView style={{flex:1}}>. Reuse an existing one; do not nest a second.

LiveKit / WebRTC

  • Do NOT call registerGlobals() yourself. The SDK calls it inside useInitialize (single-flight). Calling it again duplicates native event listeners.
  • If the app already initializes LiveKit (registerGlobals in index.js and/or a native setup()): do not add a second native setup(); keep the host's. Note that the SDK's own registerGlobals() will still run — flag this to the developer as a known double-registration risk that needs RevRag confirmation.

5. Android native integration

  1. MainApplication — call LiveKit setup in onCreate() after super.onCreate() and before React Native loads (loadReactNative(this) / load()):
    import com.livekit.reactnative.LiveKitReactNative
    override fun onCreate() {
      super.onCreate()
      LiveKitReactNative.setup(this)   // before React Native starts
      loadReactNative(this)             // (or the app's existing load call)
    }
    Missing it → runtime crash audioRecordSamplesDispatcher is not initialized!.
  2. Permissions — add nothing. The SDK's manifest merges RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, BLUETOOTH_CONNECT; LiveKit merges INTERNET, CAMERA, FOREGROUND_SERVICE, etc. The mic runtime prompt is done by the SDK during startCall. For a voice-only app you may strip camera: <uses-permission android:name="android.permission.CAMERA" tools:node="remove"/>.
  3. android/build.gradle ext must define minSdkVersion (≥ 24), compileSdkVersion, targetSdkVersion, kotlinVersion, ndkVersion — the SDK inherits them and needs NDK/CMake (it has a C++ TurboModule). newArchEnabled=true, hermesEnabled=true in gradle.properties.
  4. ProGuard/R8 (only if enableProguardInReleaseBuilds): -keep class com.revragai.embedreactnative.** { *; } (WebRTC/LiveKit keeps ship with their own consumer rules). Note the package is com.revragai.embedreactnative.
  5. Do not add com.airbnb.android:lottie manually — lottie-react-native provides it.
  6. Build: cd android && ./gradlew assembleDebug. On failure decide SDK-related vs pre-existing; fix; rebuild. Never suppress errors.

6. iOS native integration

  1. AppDelegate.swift — note the module and class casing:
    import livekit_react_native
    // in application(_:didFinishLaunchingWithOptions:) BEFORE React Native starts:
    LivekitReactNative.setup()
    (Objective-C: [LivekitReactNative setup].) import LiveKitReactNative does not resolve.
  2. Info.plist: NSMicrophoneUsageDescription is mandatory (crash without it). NSCameraUsageDescription only if video is used. ATS: allow your embedUrl host.
  3. cd ios && pod install after every dependency change (Podfile should have New Arch enabled).
  4. Fonts: run npx react-native-asset once from the app root (adds UIAppFonts).
  5. Deployment target ≥ 15.1 (16.0 on RN ≥ 0.81). Build with Xcode / npx react-native run-ios.

7. Babel & Metro validation

Do not replace configs. Confirm: presets intact; the Reanimated/worklets plugin is the last plugin in every branch; metro.config.js unchanged unless it's a monorepo (then the SDK must resolve to ONE copy of react/react-native). After changes: reset Metro cache, rebuild natively if native config changed.


8. SDK initialization (exact contract)

Place at the root component, exactly once:

import { useInitialize } from '@revrag-ai/embed-react-native';
const { isInitialized, error } = useInitialize({
  apiKey: API_KEY,                 // required (from the app's config/env system)
  embedUrl: EMBED_URL,             // optional; RevRag-provided host
  appVersion: APP_VERSION,         // REQUIRED — canonical place for it
});
  • Returns { isInitialized, error }. On failure error is a string; transient errors retry with backoff inside the SDK; auth/validation errors are fatal.
  • Fail-open: do NOT block the app on isInitialized. Render the app; the widget simply won't appear until config arrives. Log error if set.
  • Calling it from a second component re-registers (harmless GET); StrictMode-safe.
  • registerGlobals() is invoked here — do not add it elsewhere.

Wrap the tree — this nesting order matters:

<GestureHandlerRootView style={{flex:1}}>
  <SafeAreaProvider>
    <EmbedProvider navigationRef={navigationRef} showOnAllScreens excludeScreens={[…]}>
      {/* any PortalProvider / PaperProvider / BottomSheetModalProvider go INSIDE here */}
      <NavigationContainer ref={navigationRef}>…</NavigationContainer>
    </EmbedProvider>
  </SafeAreaProvider>
</GestureHandlerRootView>

navigationRef is REQUIRED for the widget to appear. The provider only learns the current screen from the navigation state listener attached through that ref (attached at mount, retried once at 1 s). Without it: the button never shows, capture screen stays unknown, startCall() fails "no EmbedButton mounted". The ref must be the same object passed to NavigationContainer, and the container must be mounted within ~1 s of the provider (mount it unconditionally; put auth gates inside via initialRouteName).

EmbedProvider renders the floating button itself — never render EmbedButton manually. Mount EmbedProvider outermost (above portal/paper/bottom-sheet providers) or the widget and highlights end up underneath modals.


9. API key validation

Verify a non-empty apiKey exists in the app's config/env mechanism and is passed to useInitialize. Do not hard-code secrets in source if an env system exists. If missing → stop, report "API key missing", ask the developer. Missing key ⇒ useInitialize returns error apiKey is required… and every request fails.


10. App version validation

appVersion is required in useInitialize (the EmbedProvider appVersion prop is an optional override; useInitialize wins). Derive it from the app's real version (package.json version, versionName, or env). Never pass ""/undefined — blank is rejected and the SDK falls back to the native build version, then the literal "unknown", and logs console.error('[Embed] No app version was supplied…'). Verify: that error line must be absent, and requests carry the X-Revrag-App-Version header / ?app_version= on /initialize.


11. USER_DATA validation

After the user is identified (login), send once per user:

import { Embed, EmbedEventKeys } from '@revrag-ai/embed-react-native';
await Embed.Event(EmbedEventKeys.USER_DATA, { app_user_id: userId /*, data?: {...} */ });

Facts the integration must respect:

  • It is persisted only after the backend accepts it — if sent offline it is not stored; send it again once online (e.g. on next app foreground / login success).
  • Without it: every other event is dropped pre-send ([Embed SDK] Skipping event payload…), cache sync stays dirty, and a call's token fetch aborts ([Embed Call] no app_user_id stored (USER_DATA event not sent yet?)).
  • startCall() still resolves true in that case (the button swallows the error) — so never treat the return value as "identified". Observe AGENT_CONVERSATION_STARTED.
  • EmbedLogout() clears identity — re-send USER_DATA for the next user.
  • Embed.Event never rejects; pass onResult(ok, err) to observe delivery.

Required order: useInitialize → init success → USER_DATA → widget config → screen attached → baseline snapshot → interactions → call.


12. Widget configuration validation

The FAB renders only after /initialize returns widget_config. Test:

CaseExpectedObserve
A. config existsFAB appears on allowed screens; agent_visible fires[Embed] /initialize response: contains widget_config
B. config null/invalidno FAB, no crash, app usable[Embed] widget_config parse failed: or empty config; nothing else
C. init failsno FAB, app fully functionaluseInitialize().error set; Initialization error:
D. delayed configapp must not assume config is ready; FAB appears when it landson a true cold start the FAB may need the next screen change to mount

Also confirm the native store: [Embed] Cache files dir: …/embed_cache/db — if instead you see 'EmbedFileStore' native module unavailable — cache is IN-MEMORY, autolinking failed (rebuild natively).


13. Ask the developer (do not guess)

After inspecting, list the actual route names you found (deepest active route names — e.g. home_tab, submit_interest; not display titles), then ask:

  1. Which screens should the RevRag widget appear on? (Offer: all-except-auth via showOnAllScreens + excludeScreens, or an explicit includeScreens list.)
  2. Which screens must never be transmitted/shown (login, OTP, payment, regulated data)? → excludeScreens (this also ends a live call on entry).
  3. Visibility mode:
    • A. Continuous (embedButtonContinuity: 'continuous', default): button and any live call persist across allowed screens; delay applied once.
    • B. Per-screen ('perScreen'): button is re-evaluated/re-delayed on every screen (collapses the card, may flicker). It does not end the call — only visibility rules or exclusion do.
  4. Should a live call survive navigating to a screen where the button is hidden? Default endCallWhenHiddenByVisibility: true ends it; set false to let it continue.
  5. App version source if it can't be inferred; API key location.
  6. Any WebView-only, native-picker, or custom-navigation flows (see §22).

Wait for answers before finalizing screen integration.


14. Screen integration (how the SDK actually detects screens)

  • Current screen = name of the deepest active route (getRootState() walked via routes[index]); never options.title. Nested tabs-in-stack ⇒ the leaf tab name, but the parent stack route flashes first while the nested navigator mounts — do not put a parent route (e.g. Home) in excludeScreens or a call may be cut on that transient.
  • Matching is exact, case-sensitive unless setScreenMatching() tiers are enabled (aliasMatching — naming variants; ancestorMatching — any parent navigator name; fingerprintScreens — capture naming only, not visibility). Prefer renaming routes to stable snake-case ids; those names are the backend's screen identities.
  • Visibility algorithm: empty screen name ⇒ hidden. If showOnAllScreens ⇒ shown unless in excludeScreens. Else shown iff in includeScreens ∪ group screens (empty ⇒ all) — and in this mode excludeScreens is NOT consulted.
  • SCREEN_STATE events only tag later analytics events with a screen name. They do not drive widget visibility or UI capture. There is no supported way to feed a non-React-Navigation router into visibility — convert to real routes or ask RevRag.
  • Every screen is walked for the durable cache regardless of visibility; only excludeScreens stops transmission/pulls for the live channel. Treat exclusion as the privacy control.
  • Navigating to the background always ends a live call.

Test: Screen A (widget appears) → Screen B → back to A; nested tabs; a modal; a reset; repeated visits — no duplicate widgets (the SDK renders one, in the topmost host).


15. Continuous vs per-screen — what to verify

Continuous: FAB stays mounted across allowed screens; a call started on A stays live on B (if B is allowed). Per-screen: FAB re-appears (after embedButtonDelayMs) per screen and the expanded card collapses; the call is still live. In both: entering an excluded screen ends the call ([Embed Call] controller endCall (public API)endCall invoked).


16. Event ordering validation

Confirm this order in the logs and that nothing fires out of order: [Embed] /initialize response: → (USER_DATA accepted) → [Embed] Cache files dir: / agent_visible[Embed] Cache screen "<name>": nodes=N → (call) [Embed Call] token fetchedconnect attempt 1/3connected in Nms[Embed Snapshot] SENT reason=baseline screen=…. Invalid patterns to catch: event before init (Embed SDK is not initialized), call before USER_DATA (§11), duplicate embedOnAgent handlers (each returns a handle — must embedOffAgent on unmount), a second EmbedProvider/EmbedButton.


17. UI tree / snapshot validation

For every configured screen, during a call, observe [Embed Snapshot] SENT reason=<baseline|route|scroll|action|interaction|overlay|speech|manual> screen=<name> bytes=N budget=N trims=N (and skipped dedup when unchanged). Off-call, the durable cache logs [Embed] Cache screen "<name>": nodes=N captures=N. Test initial screen, navigation, return, tap, text input, scroll, modal, nested and dynamic content. If nothing is captured: check the screen name (is it ''/unknown → ref problem), exclusion, mid- navigation re-arm, DROPPED oversize (node budget), or [Embed] Nav settle: … did not clear (two screens composed).

What the tree captures: buttons (anything with onPress), inputs, text, form fields, checkbox/radio/switch/slider (via accessibilityRole/accessibilityState/accessibilityValue), scrollables. It cannot see WebViews, native pickers, native-stack header titles, or gesture-only tappables.


18. Event capture validation

Automatic click tracking patches Pressable, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback (+ TouchableNativeFeedback on Android) imported from react-native. It does not patch RNGH's Pressable/RectButton, RN <Button>, or Text onPress (those still work for the agent's own taps, but emit no analytics).

Element id for analytics = props.idprops.name → generated <component>-N (testID/accessibilityLabel are ignored). Emits ANALYTICS_DATA { event_name: '<id>_pressed', metadata: {timestamp, type:'press', component, clickSequence} }, only while the widget is visible and the backend clickTracking flag is on.

The agent's targeting id (stableId) is separate: testIDnativeID/id → visible text → role. So on every meaningful control set both a testID and an id/name:

<TouchableOpacity id="submit_interest" testID="button_submit_interest" onPress={submit}>

Test: tap it and confirm the submit_interest_pressed event; test Pressable, TouchableOpacity, TextInput, ScrollView, FlatList rows (give rows unique testIDs), dynamic content. Do not invent unsupported event APIs.


19. Call validation

Preconditions the SDK enforces: widget visible on the current screen (else false + [Embed] startCall(): refused — the widget is hidden on this screen), an EmbedButton mounted (config loaded), mic permission 'granted' (SDK prompts; denied → Alert + MICROPHONE_PERMISSION_DENIED), USER_DATA stored (§11). Calls are single-flight (a tap racing a programmatic startCall cannot start two).

Validate: startCall()AGENT_CONVERSATION_STARTED → widget shows the active call → isCallActive() true → endCall()AGENT_CONVERSATION_ENDED (carries metadata.callDuration) → [Embed Call] room disconnected. Then a second call starts cleanly (no duplicate listeners; [Embed Call] end PRESSED must only appear on a real tap). Mute: N taps ⇒ N [Embed Mic] toggle: lines. Test on a real device.

Optional programmatic API: startCall({agentTriggerMode}), endCall(), isCallActive(), pauseAgent()/resumeAgent()/isAgentPaused() (pause keeps the room up, releases the mic), expandWidget() (counts as a user tap in analytics — call only on real intent), collapseWidget(), isWidgetExpanded().


20. Error / failure conditions to test explicitly

AreaCases
Initsucceeds · fails (bad key/URL/offline) · called twice · delayed — app never crashes
API keypresent · missing · empty → apiKey is required
appVersionpresent · missing · blank → "unknown" + console.error (must not happen in the final integration)
USER_DATAprovided · missing · sent offline (not persisted) · sent late · after EmbedLogout
Widget configexists · null · delayed · unparsable → no crash
Screenvalid · unknown/'' · nav before init · rapid nav · repeated · reset · modal
UI treecaptured · not captured · oversize (DROPPED oversize) · dynamic
Callbefore init · before USER_DATA · on hidden screen · success · denied mic · timeout (Connection timed out 20 s, Failed to connect after 3 attempts) · ended · repeated · backgrounded (ends)

21. Crash visibility — grep for these

audioRecordSamplesDispatcher is not initialized (LiveKit native setup missing) · Property 'DOMException' doesn't exist (LiveKit < 2.10.2 on Hermes) · attempted to access privacy-sensitive data without a usage description (iOS mic key) · [Embed SDK] react-native-reanimated … (Babel plugin) · GestureHandlerRootView / "Element type is invalid… got: undefined" (RNGH missing/too old) · 'EmbedFileStore' native module unavailable (autolink failed) · uses-sdk:minSdkVersion … cannot be smaller than version 24 · [Embed SDK] Click tracker installation failed · Media devices error: · Network request blocked by iOS App Transport Security · Unable to load script / not been registered (Metro). Never swallow a native crash; classify app-side vs SDK-side.


22. Unknown-unknowns — inspect the host for these (detect → why → do)

Work through every row. "Ask" = a developer decision.

UI / layout

  • Edge-to-edge / translucent StatusBar / react-native-edge-to-edge / provider under a padded SafeAreaView → viewport vs bounds drift near the bottom; keep EmbedProvider outermost; verify highlight/visibility on a device.
  • KeyboardAvoidingView / keyboard-controller → typing shifts layout; stale highlight, spurious bounds_moved; ask + verify.
  • Bottom sheets / drawers / overlays not declared modal (gorhom, custom backdrops, toasts) → the agent can "see" and tap through covered content; add accessibilityViewIsModal + accessibilityRole="dialog" to the sheet root.
  • Portal host above the provider (Portal.Host, @gorhom/portal, portalize) → FAB/highlight render beneath sheets; nest inside EmbedProvider.
  • iOS native-stack presentation:'modal'|'formSheet' → not an RN <Modal>; widget doesn't hop into it; verify or prefer RN Modal.
  • Disabled-by-style controls (pointerEvents="none", opacity only) → walker only honors disabled/accessibilityState.disabled; pass disabled.

Components / design system

  • Custom Button/Touchable wrappers not forwarding id/name/testID/onPress → generated click ids, weak stableIds, un-tappable; forward them (grep the design system).
  • Touchables from RNGH / RN <Button> / Text onPress → no click analytics; accept or wrap in an RN Pressable.
  • Gesture-only tappables (GestureDetector, PanResponder) → no onPress ⇒ not tappable; expose onPress.
  • onPress handlers that read args/e.nativeEvent.* → the agent invokes with an empty event; make handlers event-tolerant.
  • Disabled CTA next to similar labels → text-twin matching can press the wrong one; give disabled controls accessibilityRole="button".
  • Icon-only buttons without accessibilityLabel → positional ids that shift; label them.
  • Selection shown by style only (custom radios/chips/tabs) → verification needs accessibilityState.checked/selected; add it.
  • Composite controls (paper/NativeBase/Tamagui checkbox, radio, switch) → OK if the inner control exposes accessibilityState.checked (the verifier now checks the whole tree); custom sliders need accessibilityValue{min,max,now} + onValueChange.
  • Native pickers, DateTimePicker, WebView, native header titles → invisible to the agent; ask (text fallback or exclude).

Inputs / forms

  • Uncontrolled TextInput → capture reads value/defaultValue; verification reports set_text_not_applied; use controlled inputs.
  • editable={false} used as a picker trigger → agent types into it; use Pressable + Text.
  • OTP boxes / password with show-hide toggle → value captured whenever secureTextEntry is false; add nativeID="embed-redact-<name>" (works regardless of the toggle). Redaction is per node — tag each field, not the wrapper.
  • Masked inputs (react-native-mask-input etc.) → reformatting is tolerated for digit/letter squash; verify letters-inserted masks.

Lists / performance

  • Heavy screens (SVG charts, Lottie, large windowSize) → every view is measured on each capture (250/400 ms debounce); jank on low-end devices; only excludeScreens stops the walk.
  • testID on every wrapper / index-based testIDs → node bloat, budget trims, unstable ids; only meaningful controls.
  • Virtualized/inverted/pre-scrolled lists, FlashList, BottomSheet lists → rows unmount (not captured); scroll settle not observed; verify.
  • Full-height horizontal pagers (tab-view, pager-view) → never auto-scrolled.

Navigation-adjacent

  • NavigationContainer mounted > 1 s after the provider, or ref not shared → screen '' forever; mount unconditionally inside the same gate with the same ref.
  • expo-router → route names are segments (index, (tabs), [id]) that collide; pass useNavigationContainerRef() as navigationRef, enable ancestorMatching, rename duplicate leaves; ask.
  • React Native Navigation (Wix) / react-router / state-driven screens → no NavigationContainer ⇒ no screen detection; ask (convert to routes).
  • Multiple / independent containers or providers → only the ref'd root is tracked; last provider wins; consolidate.
  • Display-title or duplicate route names → exact-match failures; rename or setScreenMatching.
  • navigation.reset → agent's back/tab-restore falls back to goBack cleanly; fine.

Native / platform

  • AppState → background ends the call — Android fires it for camera/document pickers, UPI/payment intents, in-app browsers, other permission dialogs, split-screen. Grep Linking.openURL, image-picker, InAppBrowser inside call flows; ask.
  • Audio-session users (track-player, expo-av, video, InCallManager) → the SDK sets communication mode per call and resets after; ask.
  • Brownfield / multiple React roots / PiP / headless → one provider per root; native screens invisible; screenshots null without an Activity.
  • RNGH not natively linked → modal contents wrapped in a gesture root crash; link it.

Build / tooling

  • Expo Go → cannot run (native peers); dev build. expo-linear-gradientreact-native-linear-gradient.
  • Duplicate react/react-native copies (hoisting, pnpm) → patches hit the wrong copy; npm ls react react-native.
  • SDK imported lazily / provider not at root → touchable patches applied late remount subtrees; import the SDK first in index.js/root.
  • R8 with custom consumer rules / OTA (CodePush) native skew → keep com.revragai.embedreactnative.**; JS-only OTA cannot change LiveKit/WebRTC natives.

Runtime / lifecycle

  • Sentry/Crashlytics console capture → the SDK emits an ungated console.error when no appVersion is supplied; always pass appVersion so the line never fires.
  • Post-tap animations > 400 ms (LayoutAnimation, withTiming on press) → after-snapshot precedes the change ⇒ verification "no observable effect"; ask.
  • Strict Mode double-mount → handled (init guarded); confirm one agent_visible per screen.

Security / privacy

  • excludeScreens ≠ no capture — every screen is walked and synced to the ui-graph cache; text and non-secure values persist on disk (iOS Documents). Mark sensitive nodes (secureTextEntry for passwords/OTP — hard guarantee; embed-redact sentinel for others) and ask the developer about regulated screens.
  • Manifest merge adds mic/Bluetooth/camera permissions — strip camera for voice-only apps (§5); disclose in store listings.

23. Final checklist & report

[ ] RN/React/arch/Hermes/package manager detected      [ ] SDK ≥1.1.0 installed, android/src + ios/*.m present
[ ] Peers pinned to Stack A or B                         [ ] livekit-client: exactly one copy
[ ] Reanimated/worklets plugin last (all branches)      [ ] GestureHandlerRootView + SafeAreaProvider wrap
[ ] Android: LiveKit setup in MainApplication, minSdk≥24, NDK/Kotlin ext, (ProGuard)  [ ] Android build OK
[ ] iOS: livekit_react_native setup, NSMicrophoneUsageDescription, pod install, react-native-asset  [ ] iOS build OK
[ ] registerGlobals NOT called by host; existing LiveKit reconciled
[ ] useInitialize once at root with apiKey + appVersion; fail-open; no "No app version" error
[ ] EmbedProvider outermost, wrapping NavigationContainer with the SAME navigationRef
[ ] USER_DATA sent after login (and re-sent if offline / after logout)
[ ] Screens + mode + endCallWhenHidden + excludes chosen by developer
[ ] Widget appears on chosen screens; null/delayed config handled; one widget instance
[ ] Navigation tested (A→B→A, nested, modal, reset); continuity as chosen
[ ] UI tree captured per screen (`[Embed Snapshot] SENT` / `[Embed] Cache screen`)
[ ] Click capture tested (`<id>_pressed`); ids/testIDs set on key controls
[ ] Call start/connect/end/re-start tested on a real device; mute; cleanup
[ ] Duplicate listeners/widgets/init checked           [ ] §22 unknown-unknowns reviewed
[ ] No integration-related crashes

Final report format

Integration Status: SUCCESS / PARTIAL / FAILED
SDK version: | RN: | React: | New Arch: | Hermes: | Android: | iOS:
Dependencies added/changed (+why):
Native changes (Android / iOS):
Babel/Metro changes:
Widget config — screens: | mode: | endCallWhenHidden: | excludes:
Validation — Init: | API key: | app_version: | USER_DATA: | Widget config: | Widget rendering: |
             UI tree: | Event capture: | Call flow: | Android build: | iOS build: | Crash validation:
Unknown-unknowns found + handling:
Remaining issues (and whether app-side or SDK-side):

Do not declare SUCCESS while any critical validation is failing.