@graineai/inapp-react-native
v0.44.1
Published
Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.
Maintainers
Readme
@graineai/inapp-react-native
A Graine AI agent that runs inside your React Native app — it sees the screen your customer is on, and can act on it.
Android and iOS from one package.
npm install @graineai/inapp-react-nativeFull documentation: https://www.graine.ai/docs/in-app
Before you write any code
Add app:// to the agent's Allowed domains — dashboard → your agent →
Embed & Widgets → Allowed domains.
A mobile app sends no Origin header; there is no page, so there is nothing for
a browser to report. Without that entry every call is refused:
403 This endpoint must be called from a browser page.Nothing in your code fixes that, which is why it is the first thing on this page. The SDK detects this specific failure and says so, rather than passing the server's browser-shaped message through.
Quick start
import { GraineProvider, GraineAgentBar } from "@graineai/inapp-react-native";
export default function App() {
return (
<GraineProvider
baseUrl="https://www.graine.ai"
publishableKey="pk_live_…"
variables={{ customer_name: user.firstName }}
>
<RootNavigator />
<GraineAgentBar accent="#318CE7" name="Assistant" />
</GraineProvider>
);
}Your publishable key is in the dashboard under Embed & Widgets.
Mount the bar at your root, never inside a screen. The agent's job is to move the customer to the right screen — and a bar rendered inside one unmounts the instant it does, killing the conversation on the very action that proves the feature works.
It is a bar rather than a sheet for the same reason: a sheet covers the screen the agent is helping with, so the moment it says "tap Continue", Continue is behind it.
Tell the agent what the customer can see
One hook, beside the state your screen already holds:
useGraineScreen({
screen: "signup_email",
title: "Your email address",
journey: { name: "Sign-up", step: 2, of: 4 },
fields: [
{ name: "email", label: "Email", value: email,
status: error ? "invalid" : email ? "filled" : "empty", error },
],
});The description is withdrawn when the component unmounts, so the agent can never discuss a screen the customer has left.
Let the agent act
Declare it in the dashboard first — Embed & Widgets → What this agent may do in your app. Presets cover navigate, fill a field, highlight and retry. Saving there also pushes the tools to the agent; a declaration that is saved but not synced leaves the agent with the tools it had before, and looks like nothing happened.
Declared server-side and nowhere else: a client that could name its own tools could invent one and talk the model into calling it. The SDK may only narrow the list, never extend it.
Then implement a handler with the same name, where it belongs:
useGraineAction("fill_pan_from_aadhaar", async () => {
const value = await lookupPanFromAadhaar();
setPan(value);
return { status: "ok", data: { pan: value } };
});Answer honestly — ok makes the agent tell the customer the change is done.
A refusal with a reason is often more useful:
return { status: "refused", message: "PAN is locked after submission." };Answer within about five seconds. For slower work, return immediately and report
the outcome through the next useGraineScreen update.
Voice
The short way: no native audio module, no rebuild, and audio fixes reach you over the air.
import { WebView } from "react-native-webview";
import * as Font from "expo-font";
import Svg, { Path } from "react-native-svg";
import { GraineProvider, GraineVoiceLauncher, GraineVoiceBar } from "@graineai/inapp-react-native";
<GraineProvider
baseUrl="…"
publishableKey="pk_live_…"
navigationRef={navigationRef}
autoConnect={false} // the launcher owns the connection
loadFont={Font.loadAsync} // the SDK loads the design's typeface itself
svg={{ Svg, Path }} // …and draws its icons in the design's style
>
<RootNavigator />
<GraineVoiceLauncher
webView={WebView}
autoStart
onError={(m) => console.warn("[graine]", m)}
>
{(api) => <GraineVoiceBar api={api} />}
</GraineVoiceLauncher>
</GraineProvider>react-native-webview is the only peer dependency and it autolinks. The
microphone and speaker live in a hidden one-pixel WebView — your customer never
sees a web page. Set autoConnect={false}: leave the provider connecting too
and the agent answers twice, so the customer hears two greetings and you are
billed for two conversations.
Audio travels on a WebSocket by default. Set Embed & Widgets → Call audio to
High quality for WebRTC — Opus, a jitter buffer and the device's own echo
canceller — which carries screen context just the same and falls back on its own
if it cannot connect. See INTEGRATION.md.
Bringing your own audio
Only if you already have a native audio module and want it used instead.
<GraineProvider baseUrl="…" publishableKey="pk_live_…" voice>const { listening, start, stop } = useGraineVoice(adapter, {
onTranscript: (text, final) => final && setHeard(text),
});Audio is a pluggable four-method AudioAdapter, so this package never forces a
particular native audio module into your build. liveAudioStreamAdapter wraps
react-native-live-audio-stream.
Capture must be 16 kHz mono PCM16. Interruption is handled for you.
Buffer before you play. A player that plays each frame as it arrives will
break up: a WebSocket gives no timing guarantee, so the gap between frames
varies with the network and a player with no cushion runs dry in that gap. Hold
at least 150ms (MIN_PLAYOUT_BUFFER_SECONDS) before starting, and when the
queue does run dry, refill to that again before resuming — otherwise one hiccup
becomes continuous stuttering for the rest of the turn.
Report what you are holding through bufferedSeconds(), so the agent knows
when its reply has actually been heard rather than just received.
A bigger chat window
How big the chat opens is set per agent in the dashboard — Embed & Widgets →
Appearance → Chat window → Window size: Compact, Large or Full screen — and
GraineVoiceBar draws it with no code:
| Size | On a phone | On a tablet | |---|---|---| | Compact | The window above the bar, as always | The same | | Large | A sheet over most of the screen, the app dimmed behind it; drag it down or tap outside to close | A 420-wide panel in the bottom corner | | Full screen | The whole screen, closed with the chevron at the top left | The whole screen |
Your app can choose otherwise, for every chat or for one:
<GraineProvider baseUrl="…" publishableKey="pk_live_…" chatSize="large">
const client = useGraineClient();
client.openChat({ size: "full" }); // opens the chat now, at this size, this onceThe size passed to openChat wins for that open, then the provider's
chatSize, then the dashboard's. Moving between the call and the chat keeps it.
Pass safeArea={useSafeAreaInsets()} (react-native-safe-area-context) to the
bar for exact insets on a sheet or a full screen.
For a chat that is a screen of your own app rather than part of the bar:
<Stack.Screen name="Help">
{({ navigation }) => <GraineChatScreen onClose={navigation.goBack} />}
</Stack.Screen>While it is on screen the customer is in the chat and the bar steps aside.
Privacy
Identifiers are masked on the device before any screen data is sent — PAN, Aadhaar, card and account numbers, phone numbers and email addresses. The agent is told whether a field is filled, empty or rejected, not what is in it.
Add your own formats once, at startup:
import { addMaskRule } from "@graineai/inapp-react-native/mask";
addMaskRule(/MEM-\d{6}/, "[MEMBER_ID]");maskString(text) and maskDeep(value) from the same module run that masking —
your rules included — on anything you send somewhere yourself; a Date comes
back as its ISO string, as JSON would send it.
The SDK never reads the screen by itself — it reports only the fields you pass
to useGraineScreen. There is no screen recording and no accessibility
scraping. It never opens the microphone until you call start(), and never
requests a permission on your behalf.
A publishable key inside a mobile app is public. Anyone can extract it from an APK or IPA, and the web domain allowlist does not apply to mobile apps. Rate limits, single-use connection credentials and your account's credit limits are what bound it. See the security notes before launch.
Conversations continue
A customer you identify() who comes back — later today, tomorrow, on their
other phone — finds the chat where they left it, and the agent picks up from
there instead of greeting them again. The chat opens with the recent messages
(the last 50, kept for a while after the last one), earlier calls show as one
"Voice call · 6:05 PM" row, and day lines say Today, Yesterday or the date.
Nothing to set up: the provider and the SDK's chat windows draw it.
A support-style chat can start over: newConversation() (on the client, or
from useGraineAgent()) hangs up, lets go of the kept conversation for this
agent and customer, clears the chat and opens a fresh one with the welcome —
what the agent remembers about the customer stays. The chat window's header
offers it (a compose icon) while it shows an earlier conversation. Each agent
in your app keeps its own conversation.
Drawing your own chat? Each turn says restored: true when it came back from
an earlier conversation and channel: "voice" when it was said on a call;
chatTurnRows(messages) gives the rows to draw. forgetMemory() clears the
chat on screen, as signing out or another person signing in does. Anonymous
customers, and customers with memory off, start a fresh chat every time.
Booking a call from your app
When your workspace has Graine ring its customers and the agent lets them book (Calls & scheduling, in the dashboard), the SDK draws the booking screen itself — "Call me at…" with no screen of your own:
const { bookingAvailable, openBooking } = useGraineAgent(); // or useGraineScheduling()
{bookingAvailable ? (
<Button
title="Call me"
onPress={async () => {
const call = await openBooking({ reason: "Card replacement" }); // null: closed without booking
if (call) console.log("Booked", call.id, call.at);
}}
/>
) : null}The provider draws the sheet in the agent's design, light or dark: the day
(chips from today to the furthest day the agent allows), the time (a grid of
times every 15 minutes, only the ones the agent's rules allow), the phone's
time zone, "What's it about? (optional)" — 120 characters, kept with the call
for the agent, never in the push; reason fills it — and Book call. A
refusal stays in the sheet as one line: "Pick a time at least 5 minutes from
now", "Pick a time within 14 days", "That time is outside the hours we can call
— pick another", "You already have 3 calls booked — cancel one first", "Sign
in to book a call", "Couldn't book just now — try again". Once it is booked,
the sheet says when the agent will call, and Done closes it. A sheet on a
phone, a centred panel on a tablet.
Presenting it yourself: <GraineBookingSheet visible={open} onClose={…}
onBooked={(call) => …} reason="…" />, inside the provider.
bookingAvailable (also client.bookingAvailable) is each agent's own: true
only when Graine rings this phone (the workspace's In-app calling, for this
platform), the agent lets customers book, and the customer is identified with
the trait the agent's Identity setting names. The SDK's chat windows
(GraineVoiceBar's and GraineChatScreen) show the customer's booked calls in
an Upcoming calls card, each with Cancel, and Book a call under them — or
on its own in the card's place when none are booked — where booking is
available; none when the design hides the booked-calls card. To list or cancel
calls in your own screens, useGraineScheduling().
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 comes in its own package, @graineai/inapp-react-native-calls,
which runs Graine's native call engine and shares this SDK's screen context and
actions (GraineCalls.attach(client)). It needs a new store build; this SDK
stays over-the-air. https://www.graine.ai/docs/in-app/calling#react-native
Requirements
- React Native 0.68+
- React 17+
API
| | |
|---|---|
| GraineProvider | Connects the agent. Set voice for spoken conversations. |
| useGraineAgent() | Connection state (connected, connecting, reconnecting, failed once it gave up), transcript, send(), retry() and clearChat() (the screen only). |
| useGraineScreen(context) | Report what this screen shows. |
| useGraineField(name, field) | One input as it changes — value, error, status. |
| useGraineAction(name, handler) | Implement one action the agent may call. |
| useGraineVoice(adapter, opts) | Microphone, playback and barge-in. |
| useGraineEvents(fn) | Conversation lifecycle, action_completed, friction_detected, mic_denied, and error (a start that failed, or what the voice launcher reports to onError). |
| useGraineTrack() / useGraineTap() | Product events and taps the agent knows about but does not interrupt for. |
| useGraineIdentify() | Who the customer is — it reaches the greeting and the call record. |
| useGraineHighlight() / useGraineWidget() | Let the agent point at things and draw components. |
| GraineVoiceLauncher | The voice engine: WebView, microphone, WebRTC or WebSocket. |
| GraineVoiceBar | The voice bar in the brand's design, as the dashboard previews it. Mount inside GraineVoiceLauncher. |
| GraineChatScreen | The chat as a screen of your own app, in the brand's design. |
| bookingAvailable / openBooking({ reason }) / GraineBookingSheet | Booking a call: whether to offer it, and the SDK's own booking sheet. See Booking a call from your app. |
| chatSize on the provider / client.openChat({ size }) | How big the chat opens: "compact", "large" or "full". See A bigger chat window. |
| fab={{ webView }} on the provider / GraineFab | The floating button, mounted for you; opens the conversation, folds back on end. |
| captureTaps / GraineTouchCapture | Every tap tracked automatically — no per-button wrapping. |
| useGraineInit() | { isInitialized, error }. |
| GraineAgentBar | Optional default UI. Mount at the root. Replace it with your own. |
| addMaskRule(pattern, token) | Mask an app-specific identifier format. |
Friction detection — the agent notices a stuck customer (idle, repeated
field errors, back-and-forth, rage taps, error banners) and speaks first. Rules
are JSON, per screen, configured in the dashboard; pass friction on the
provider only for the screens your app knows better. DEFAULT_FRICTION,
resolveFriction and FrictionDetector are exported for hosts that want the
engine on their own timers.
Every reporting hook works outside a provider too — call setDefaultClient(client)
once if your provider wraps only your bar.
GraineInAppClient is exported for apps that want the transport without the
React layer: setScreen, clearScreen, updateField, removeField, identify, track, reportEvent,
registerAction, executeAction, configureFriction, friction, session.
Support
https://www.graine.ai/docs/in-app
License
MIT
