Graine AI

Install the SDK

Add the Graine in-app agent to a React Native app — provider, launcher, and the one decision you must make before connecting.

Before you install: two dashboard steps

Both are easy to miss and each produces a confusing failure. Do them first.

1. Give the app its own agent

Clone your website agent rather than pointing the app at it. Actions are declared per agent, and every surface that agent serves is offered all of them — so a shared agent hands your marketing site a fill_pan tool it can never run. Why this matters.

Cloning keeps the prompt, knowledge base and voice; you then change only what differs. Two agents also means two sets of analytics, which is what you want the first time you ask whether the app conversations go better than the web ones.

2. Add app:// to the agent's allowlist

Open Embed & Widgets → Install & domains on the app's agent and add the entry app://.

A native app is not a web page. It has no Origin header to send, and there is no header a client could send that we would be willing to trust instead — so the allowlist cannot identify it the way it identifies a website. app:// is how you say "this agent may also be reached from a mobile app", and it is an explicit opt-in rather than a default because an agent reachable without an origin is reachable from anything that can hold the key.

Skip this and the SDK connects to nothing. The connection is refused with no_origin, the launcher never renders, and the app looks like the key is wrong. An empty allowlist refuses everything for the same reason — a new embed that accepted every origin would let any site on the internet mount your agent and spend your call credit.

What actually bounds a mobile app is the publishable key, per-key and per-IP rate limits, and a single-use connection ticket. A key inside an APK or IPA is extractable, so treat it as public — see Privacy and security.

Install

npm install @graineai/inapp-react-native

One package covers Android and iOS. No native module, no pod install, no rebuild — the audio path lives in a WebView pointed at a hosted page, so an upgrade is this plus an over-the-air update.

Do not pin 0.27.0

It imports a native module it does not declare and breaks expo export. Any later version is fine; what each one changed is in the changelog.

Wrap your app

App.tsx
import { useRef } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { WebView } from "react-native-webview";
import {
  GraineProvider,
  GraineVoiceLauncher,
  GraineAgentBar,
} from "@graineai/inapp-react-native";
 
export default function App() {
  const navigationRef = useRef(null);
 
  return (
    <GraineProvider
      baseUrl="https://www.graine.ai"
      publishableKey="pk_live_…"
      navigationRef={navigationRef}
    >
      <NavigationContainer ref={navigationRef}>
        <RootNavigator />
      </NavigationContainer>
 
      <GraineVoiceLauncher webView={WebView} autoStart>
        {() => <GraineAgentBar />}
      </GraineVoiceLauncher>
    </GraineProvider>
  );
}

navigationRef is the line that makes this an in-app agent rather than a voice widget: the SDK reads the current route itself, so the agent knows which screen the customer is on without a call on every screen — including the screen you add next year when nobody remembers this page.

It must be the same ref you pass to NavigationContainer, and GraineProvider must wrap it. Without it the agent still talks; it just has no idea where anyone is.

Get publishableKey from Embed & Widgets on the app's agent — the one you cloned above, not the website's. Each agent has its own key, allowlist, appearance and action list.

Colour, title and logo come from the dashboard's Appearance section, not from props, so a brand change reaches every app without a release.

Install react-native-webview alongside the package. It autolinks and needs no setup code; it is what lets voice work without a native audio module. See Voice for why.

To tell the agent who the customer is, call useGraineIdentify() after sign-in rather than passing variables at construction — the provider mounts at launch and the customer signs in afterwards.

Connect at launch, not on tap

GraineProvider connects when your app starts. Leave it that way.

An agent that only connects when the customer taps the launcher can never be the one to speak first — and speaking first, at the moment somebody stalls, is most of what an in-app agent is for.

Connecting is cheap and silent. Nothing is shown to the customer, no microphone is opened, and no conversation is billed until there is one.

Voice or text

Decide before connecting:

<GraineProvider baseUrl="…" publishableKey="pk_live_…" voice>

This is not a per-message choice. Voice and text are different sockets, and a text socket has no speech synthesis at all — so an agent connected for text will answer a voice session in writing and never make a sound. Switching later means reconnecting.

See Voice for microphones and audio.

The session follows the app

One session per client, and it ends when the app leaves the foreground.

Backgrounding does not unmount a React tree, so without this the WebView stays mounted with the microphone open and the conversation still billing while nobody is listening. iOS is the worse case, not the better one: it suspends the WebView's audio without telling the socket, so the call survives as a leg that can no longer hear anything and bills for silence.

<GraineProvider endOnBackground={false}>   {/* default: true */}

The session ends on background, never on inactive. iOS reports inactive for the app switcher, a pulled-down notification, and the moment a permission dialog appears — ending on it would mean asking for the microphone hangs up the call that asked for it.

Text sessions reconnect when the app returns. Voice does not auto-resume: a call is something a person chose to start, and starting one again because they came back to the app is a microphone opening unasked. connected goes false — draw your UI from that.

connect() is idempotent, so calling it twice returns the same session rather than opening a second socket, and close() sends a stop frame before dropping the socket so the conversation is filed as ended rather than dropped.

Bring your own UI

GraineLauncher is a working default, not a requirement. Everything it draws is built on the same public hooks you have:

import { useGraineAgent } from "@graineai/inapp-react-native";
 
function MyAssistantPanel() {
  const { connected, messages, send } = useGraineAgent();
  // …your own design system
}

Most teams with a brand to honour replace it. That is the expected path.

Check it works

  1. Run the app and open the launcher.
  2. Send "hello". The agent should reply within a second or two.
  3. Open Call history in the dashboard — the conversation appears there.

If nothing happens, the console carries the reason. Errors are logged loudly for you and shown to nobody: a misconfigured agent renders no launcher at all, rather than a launcher that opens onto an error.

What you seeWhyFix
No launcher, no_origin in consoleThe allowlist has no app:// entryAdd app:// under Install & domains
No launcher, no_domains_configuredThe allowlist is empty, which denies everythingAdd app://, plus any web origins that agent serves
No launcher, error in consoleThe key was rejectedCopy the key again from Install & domains
The agent offers to do things the app cannotThe website agent is being reusedClone it and give the app its own
Launcher, but no replyThe agent has no web-call variantRe-save the agent in the dashboard to create one
Replies in text during a voice sessionConnected without voiceAdd the voice prop to GraineProvider

Next steps

On this page