Graine AI

React Native

The React Native SDK in full — install, every prop, hook and option, calls, your design, and what to check when something behaves oddly.

Integrating for the first time? Quickstart is four steps. This page is the surface area: every prop, every hook, and the troubleshooting table.

Install

npm install @graineai/inapp-react-native react-native-webview

react-native-webview autolinks. There is no native code to write — no MainApplication.kt edit, no AppDelegate.swift edit, no pod install beyond the usual, no ProGuard rule. The app does declare the microphone, because this package carries no manifest of its own: RECORD_AUDIO and MODIFY_AUDIO_SETTINGS in AndroidManifest.xml, and NSMicrophoneUsageDescription in Info.plist — with Expo, android.permissions and ios.infoPlist in app.json (Voice → Microphone permission). Expo SDK 50+ works in a development build; Expo Go cannot load react-native-webview, so voice will not run there.

React Native 0.71+, React 18+.

Configuration options

GraineProvider props

PropTypeRequiredDescription
childrenReactNodeYesYour app, usually NavigationContainer and below
baseUrlstringYeshttps://www.graine.ai unless self-hosted
publishableKeystringYesFrom Agent → Embed & Widgets. This selects the agent — one key per agent; there is no agentId prop.
navigationRefrefRecommendedSame ref as NavigationContainer, for route tracking
includeScreensstring[]NoRoute names where the launcher may appear; omit for all
launcherDelayMsnumberNoDelay before showing the launcher on an eligible screen
visibilityobjectNoGroups, continuity, per-group delays, insets — see Launcher visibility
autoConnectbooleanNoConnect at launch. Required for proactive help; default true
voicebooleanNoOnly for the bring-your-own-audio path, not for GraineVoiceLauncher
surface"voice" | "chat"NoOpen on the call or the chat, where the dashboard allows both. It cannot add a surface that was turned off.
onSurfaceChange(s) => voidNoThe customer moved between the call and the chat
onProactive(text) => voidNoThe agent spoke first — draw your own nudge
endOnBackgroundbooleanNoEnd the session — and, from 0.40.1, the call — two seconds after the app leaves the foreground. Default true. Turn it off only when your app keeps the call alive itself; see In-app calling
appVersionstringRecommendedWhich build the conversation happened in; lands on the call record
variablesRecord<string, string>NoPrompt variables your app knows at launch, the same for every user. The signed-in user's details go in setVariables — Send your user's details to the agent
frictionFrictionConfigNoThis app's friction rules, layered over the dashboard's — see Friction detection
fab{ webView, position?, size?, accent?, label?, overlay?, prewarm? }NoMount the floating button for you. position is bottom-right (default) or bottom-left. prewarm (default true) loads the voice engine before the tap, so connecting takes ~400ms instead of ~6s.
captureTapsbooleanNoTrack every tap automatically. Default on when fab is set.
loadFont(fonts) => PromiseRecommendedexpo-font's Font.loadAsync, as it is. The SDK then loads the design's typeface itself — only the faces the design uses, from Graine — and redraws when they arrive. Without it, the system font. 0.38.0
svg{ Svg, Path }Recommendedreact-native-svg's two components. The SDK then draws its icons in the design's icon style; without it, plain glyphs. 0.38.0
fontsRecord<string, string>NoFamilies your app already bundles, by the dashboard's name → your registered fontFamily. Yours win over a download
chatSize"compact" | "large" | "full"NoHow big the chat window opens, over the dashboard's choice; client.openChat({ size }) wins for one open — Window size. 0.41.0

GraineVoiceLauncher props

PropTypeRequiredDescription
webViewcomponentYesWebView from react-native-webview
autoStartbooleanNoStart the call as soon as the page is ready
onCaption(c) => voidNoEach line as it is said
onCallState(s) => voidNo{ connected, connecting }
onEnded() => voidNoThe call is over. Fires once
onMicDenied(reason) => voidNoThe microphone was refused or absent
onError(message) => voidNoVoice could not be set up — key rejected, agent not enabled for apps
transport"ws" | "rtc"NoWhich transport carries audio; omit and the dashboard decides
childrenfunctionNoReceives the voice API — see below
timing{ stallMs, bootMs, hardCapMs, retryDelaysMs, endWatchdogMs, slowConnectMs }NoSilence before a load is abandoned, a downloaded page's grace to say ready, the ceiling on one attempt, the reload ladder; slowConnectMs (default 4000) is when a connect becomes slowConnect. A load still arriving is never abandoned
activebooleanNoDefault true. Mount early with false so the engine page loads before the customer needs it (it registers nothing while idle); flip to true when your UI opens — with autoStart that places the call, without it the registration is warmed so the tap is instant. Back to false ends a live call, unconditionally.

The voice API (children)

Member
connected / connecting / mutedCall state
readyA call can be started right now
start() / end() / setMuted(m)Place, hang up, mute
captions / captionThe transcript, and the current turn
captionsOn / setCaptionsOn(on)The CC switch, defaulting on
clearCaptions()Forget the transcript without ending the call
phaseidle · connecting · reconnecting · thinking · speaking · listening — one thing to draw from
connectivity / retry()Whether the page itself loaded, and a way to try again
qualityThe connection in one word: excellent · good · poor · lost · unknown
networkonline · offline · unknown, from the engine's own view of the device
reconnectingThe call was interrupted by the network and is being re-established
errorSet when the call cannot work until something is fixed — a key or agent that is not set up for in-app calls. 0.38.0
micDeniedThe customer refused the microphone on the last start. 0.38.0
connectStageWhere a call being placed is: registering · dialling · retrying · switching, or null. retrying is a fresh connection after an unanswered first try — on mobile data, usually a connection the phone dropped while idle. 0.40.0
slowConnectThe customer has been waiting on a connect for timing.slowConnectMs. Say so ("Still connecting…") rather than spin. 0.40.0
callFailedThe last start could not be connected on either transport. start() tries again. 0.40.0

Captions and hanging up →

GraineVoiceBar props

The call's bar, drawn by the SDK in the brand's design — the dashboard Garage's in-app preview, for real. Mount it in GraineVoiceLauncher's render prop. There is nothing about its look to pass: the design, typeface, colours, heading, logo and loader all come from the dashboard. 0.38.0

PropTypeDescription
apivoice APIWhat the launcher's render prop hands you. Required
onClose() => voidAfter a hang-up or a close, once the goodbye has left the speaker
bottomInsetnumberHow far above the bottom edge it sits, clear of your tab bar. Passed, it wins; otherwise the per-flow inset, then 96
draggablebooleanLet the customer drag it up and down. Default true
placement / onPlacementChange{ bottomOffset }A position your app persisted, to restore / the customer let go somewhere new — persist it
namestringWhat spoken labels call the agent ("Start talking to Asha"). The name on the bar is always the dashboard's heading
accessoryReactNodeAnything that rides on the bar, directly above it — a button that moves the conversation to a call, say. It moves with the bar and is not clipped by it; GraineBarAction (below) draws one in the design. 0.40.2

GraineBarAction

A button for the bar's accessory, drawn in the brand's design — the accent, the corners, the typeface and the icon style the dashboard chose — so your app decides nothing about how it looks, as with the bar itself. 0.40.2

<GraineVoiceBar
  api={api}
  onClose={onClose}
  accessory={<GraineBarAction label="Call Asha" icon="call" onPress={callTheAgent} />}
/>
PropTypeDescription
labelstringWhat it says, and what a screen reader reads. Required
icon"call" or "chat"The SDK icon in front of the label. Default "call"
onPress() => voidWhat a tap does. Required

It rises above the keyboard on its own, draws any component the agent sends above the caption, and switches between the call and the chat when the dashboard allows both.

GraineLauncherButton props

The design's launcher on its own — its shape, glyph or logo, shadow or hard offset — for an app that opens GraineVoiceBar itself rather than handing the flow to fab. 0.38.0

PropTypeDescription
onPress() => voidOpen your bar. Required
styleViewStyleWhere it sits, e.g. { position: "absolute", right: 16, bottom: 24 }
labelstringWhat a screen reader says. Default "Talk to" and the heading
sizenumberDefault 56

GraineAgentBar props

The bar for a chat-first integration on the provider's own socket (for a call, use GraineVoiceBar in GraineVoiceLauncher). Everything here is optional; the dashboard's design is the source and a prop is an override.

PropTypeDescription
name / accent / avatarUrlstringOverride what Appearance set
bottomInsetnumberWhere it sits with no per-flow inset (default 96)
hiddenbooleanHide it without unmounting the provider
captionsOnbooleanStart with captions on
scheme / theme"auto" | "light" | "dark" / Partial<GraineBarTheme>Palette
draggablebooleanLet the customer drag the bar up and down — as one unit with its panel and any component. Default true
placement{ bottomOffset }A position your app persisted, to restore
onPlacementChange({ bottomOffset }) => voidThe customer let go somewhere new — persist it

The bar rises above the keyboard on its own, from wherever the customer left it.

Hooks

HookFor
useGraineAgent()Connection state, transcript, mute, send a turn. failed — the chat gave up (its first connect failed, or Graine ended or refused the conversation; never a network drop) — with retry() to connect again, and clearChat() to empty the chat on screen (only the screen: the conversation goes on). 0.43.0
useGraineSurface()Which surface is open — the call or the chat — and how to move
useGraineVariables()Set prompt variables from the app's own data while it runs — the user's name, age, plan, under the names the agent's prompt reads: Send your user's details to the agent
useGraineConnection()The connection's quality and whether the device is online
useGraineTheme()The brand's design as tokens — colours, type, corners, spacing, shadows — for the device's light or dark, for anything you draw beside the agent
useKeyboardLift()Pixels to add to a bottom-anchored bar's bottom while the keyboard is up — measured, right on iOS and both Android modes
useDraggablePlacement({ baseBottom, initialOffset, onChange })Let the customer drag your own bar up and down: spread panHandlers and onLayout on its root, add offset to its bottom. The SDK owns the gesture, the bounds and the tap-versus-drag rule
useGraineIdentify()Who the customer is
useGraineScreen()What is on the screen
useGraineField()One input, as it changes — value, error, status
useGraineAction()One handler per declared action
useGraineTrack()Product events that inform but do not interrupt
useGraineEvents()Conversation lifecycle, action outcomes, friction signals
useGraineTap()Wrap an onPress so the tap is tracked (and rage taps detected)
useGraineReady()Whether the session has resolved
useGraineInit()The same, as { isInitialized, error }
useGraineVoice()Only if you bring your own native audio

A component the agent draws is useGraineWidget()'s; client.submitWidget(widgetId, data, summary) answers one from your own UI, and client.presentWidget(frame) / client.presentPendingWidget(hint) draw one (or its loader) yourself — a preview, a test. 0.43.0 — Components

Calls like WhatsApp

A call that rings the customer's phone — CallKit and VoIP pushes on iOS, a ringing call notification and call screen on Android — and goes on with the screen off runs on the native call engine, in its own package:

npm install @graineai/inapp-react-native-calls
cd ios && pod install
await GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Acme Assistant" });
await GraineCalls.startCall({ reason: "card_blocked" });   // the customer calls the agent

It carries native code, so it needs a new store build; this SDK stays over-the-air. The pushes, the iOS capabilities, attach(client) and your server's side are in In-app calling → React Native.

Calls over WebRTC (optional)

Calls — the customer calling the agent, and a call Graine rang — carry their audio over a WebSocket unless you build with WebRTC. Turn it on per platform, then rebuild the app (it is native code):

# ios/Podfile, at the top — then pod install
ENV["GRAINE_WEBRTC"] = "1"
# android/gradle.properties
graineWebRtc=true

On a build server (EAS, CI), GRAINE_WEBRTC=1 in the build's environment turns it on for both. Nothing else changes — your code, the ring, the call screen and the outcomes stay the same. Calls package 0.4.0.

  • What the customer notices: a call that holds up better on mobile data — a jitter buffer, loss concealment, the phone's own echo canceller. Over WebRTC the agent's voice arrives at telephone bandwidth today.
  • When WebRTC cannot carry a call, it continues on the WebSocket inside the same ring and call screen: no second ring, no error. useGraineCall() says which one carried it — transport ("webrtc" or "websocket") and transportFallbackReason.
  • Choose with GraineCalls.configure({ callTransport }): "auto" (the default — WebRTC in a build that has it), "websocket" or "webrtc". The agent's In-app call audio setting (Calls & scheduling) — WebRTC (recommended) or Standard — can move every install's calls to the WebSocket from their next start, without a release.
  • Size: about 12 MB installed on an iPhone, about 7.4 MB on a 64-bit Android phone — a prebuilt WebRTC library whose symbols are renamed, so it never clashes with another copy of WebRTC in your app (a video-calling SDK, say). CocoaPods installs one version of it: a library that pins it at another version cannot be installed beside it.
  • Android adds ACCESS_NETWORK_STATE to your manifest (a normal permission: nothing is asked); the R8 rules ship with the package. Licences: WebRTC is BSD 3-Clause with Google's patent grant — list it in your app's open-source licences.

More: Calls over WebRTC.

Without React Navigation

Omit navigationRef and call useGraineScreen yourself on each screen — it carries screen as well as the fields, so nothing is lost except the automatic part. Everything else is unchanged.

Your design, in your app

The design chosen in the dashboard (Agent → Embed & Widgets → Appearance) is drawn by the SDK itself — the voice bar, the caption, the chat, the launcher and every component — exactly as the dashboard's preview shows it. Change the design or the typeface there and every phone follows the next time your app starts; nothing in your app names a colour or a font.

Mount the SDK's bar inside the voice launcher, and hand the provider the two platform abilities it cannot import itself — registering a font, and drawing a vector — once:

import * as Font from "expo-font";
import Svg, { Path } from "react-native-svg";
import { GraineProvider, GraineVoiceLauncher, GraineVoiceBar } from "@graineai/inapp-react-native";
 
<GraineProvider client={client} loadFont={Font.loadAsync} svg={{ Svg, Path }}>
  <GraineVoiceLauncher webView={WebView} active={open} autoStart>
    {(api) => (open ? <GraineVoiceBar api={api} onClose={() => setOpen(false)} /> : null)}
  </GraineVoiceLauncher>
</GraineProvider>
  • Typefaces. With loadFont, the SDK fetches the faces the design draws with — only those, from Graine — and redraws when they arrive. Until then, and if one never arrives, it draws the system font. A family you already bundle can be named in fonts ({ Manrope: "Manrope-Regular" }); yours wins.
  • Icons. With svg, the microphone, hang-up, send and chat icons are drawn in the design's icon style. Without it, plain glyphs.
  • Your own UI. useGraineTheme() returns the resolved tokens — colours, type, corners, spacing, shadows — for anything you draw beside the agent.

How long connecting takes

Four things happen between the tap and the agent's first word. On a current app, three of them are done before the tap.

StepWhenTypical
The engine loadsBefore the tap — GraineFab loads it when it opens; a custom bar mounts GraineVoiceLauncher at launch with active={false} and turns it on when it opens2–4 s, out of sight
The line is preparedBefore the tap — at launch, again before it expires, and whenever the app returns to the foreground (0.36.0); at the tap on a bar that starts by itselfabout 1 s, out of sight
The microphone promptAt the first tap only, alongside everything else the call is waiting fornone once granted
The call connectsAt the tapabout 0.2 s

The engine connects on the first network route that works, within 1.5 s — it never waits for a VPN or an idle mobile interface to be tried, which on its own can take forty seconds.

If "Connecting…" lasts more than about two seconds on a current app, the engine is being loaded at the tap. Mount it earlier.

Troubleshooting

SymptomWhat to check
Agent talks but does not know the screennavigationRef must be the same ref as on NavigationContainer, and GraineProvider must wrap it
"Sorry, I can't help with that here"Action not declared in the dashboard, name does not match useGraineAction exactly, or the screen did not list it. The client may only narrow the declared list, never extend it
No sound on iPhoneThe ringer switch. Set playsInSilentModeIOS: true and allowsRecordingIOS: true while a call is live
Launcher never appearsCurrent route is not in includeScreens or any group, or a delay is still running. Omit includeScreens to test
Agent interrupts itself after a wordEcho. Lower the speaker volume, or use earphones to confirm
Agent mishears everythingBring-your-own-audio only: the capture rate is not 16 kHz. A wrong rate produces confident nonsense, not an error

Best practices

  • One provider, at the root. Not per screen.
  • Call identify() as soon as you have a signed-in user, and again on account switch — with the field the agent's Who is the user? setting names (user_id by default), so the agent remembers them across chats, Talk and calls: Memory.
  • Send the user's details under the prompt's own names with setVariables, and clear them with setVariables(null) on sign-out — Send your user's details to the agent.
  • Give every reported field a status. It is the difference between useful proactive help and a generic nudge.
  • Return a message from every action saying what actually happened — the agent repeats it, so a vague result becomes a vague promise.
  • Hide the launcher on sensitive flows (auth, payment entry) with includeScreens or groups.
  • Plan visibility early. Retrofitting group rules is harder than wiring them during integration — see Launcher visibility.
  • Match route names exactly. They are case-sensitive.

Why there is no native setup

The SDK needs no edit to MainApplication.kt or AppDelegate.swift and has no native module of its own: beyond react-native-webview and the microphone's two declarations, nothing native changes. The audio runs in the same hosted engine as the web widget, which already does capture, playback, buffering, decoding and echo suppression — so an improvement to the audio reaches your app without an app-store release, and updating the SDK is an npm install.

The trade, stated plainly: a WebRTC call uses the phone's own echo canceller; on the fallback path a level-based guard stands in for it, and gives up when the speaker is very loud. Earphones or a lower volume settle it.

Next steps