Graine AI

React Native SDK reference

Every export of @graineai/inapp-react-native 0.44.1 and @graineai/inapp-react-native-calls 0.5.1, grouped by what you are building — components and their props, hooks, the client, functions, constants and types: signatures, parameters, what each returns, the errors it raises and what to expect.

Every export of the React Native SDK, @graineai/inapp-react-native 0.44.1, and of its calls package, @graineai/inapp-react-native-calls 0.5.1 (which carries the call engine of Graine's iOS and Android SDKs 0.10.1), read from their source. The page is grouped by what you are building — installing, identity and memory, the chat, the voice bar, several agents, screen context and actions, calls, booking, the scheduled-calls list, design, going live, events, errors — and every component, hook, function, constant and type sits with the use it serves. Each entry opens with what it is for, then its signature, parameters or props, what it returns, the errors it can raise and an example; each type lists every field with what to expect. The same API across every platform, topic by topic, is the SDK reference; a step-by-step set-up is React Native.

Requirements, install, permissions and initialisation

Use this section to add the packages, declare what the operating systems ask for, and mount the agent once at the root of your app.

WhatDetails
Packages@graineai/inapp-react-native 0.44.1 — the agent: chat, the voice bar, screen context, actions, booking. @graineai/inapp-react-native-calls 0.5.1 — optional: calls that ring like a phone call (CallKit on iOS, a full-screen ring on Android), with the call engine of the native SDKs 0.10.1.
RequiresThe SDK: React Native 0.68+ and React 17+ (its peer dependencies), and react-native-webview for voice — the voice engine runs in a WebView you pass in, so a chat-only app does not need it. The calls package: React Native 0.71+, React 18+, a native build (not Expo Go), iOS 15+ (its pod) and Android 7 / API 24+ (its Gradle module, Java 17). Node 18+ for development.
Recommended@react-native-async-storage/async-storage, passed as storage: it keeps this install's id between launches, which memory, conversations that continue and booked calls need (the calls package keeps one itself when it is linked). expo-font, passed as loadFont: the SDK loads the design's typefaces. react-native-svg, passed as svg: the SDK's icons are drawn in the design's icon style. Without the last two: the system font and plain glyphs.
OptionalWebRTC voice for the bar (RtcVoiceSessionController, or transport="rtc"): react-native-webrtc and the WebRTC peer dependency the package declares as optional — a native module, so it needs a store rebuild.
WebRTC callsOptional, per app build: the calls package's calls — the customer calling the agent, and the customer answering a call Graine rang — carry their audio over WebRTC. One line in ios/Podfile and one in android/gradle.properties, nothing to install from npm, then a rebuild — see WebRTC calls.
ExpoA development build (SDK 50+). Expo Go cannot load react-native-webview or the calls package. The calls package ships an Expo config plugin that writes a call's iOS setup at prebuild; the SDK package has none — put its microphone settings in app.json (below).

Install

npm install @graineai/inapp-react-native react-native-webview @react-native-async-storage/async-storage
npm install @graineai/inapp-react-native-calls      # calls that ring (optional)
npm install expo-font react-native-svg              # the design's typeface and icons (recommended)
cd ios && pod install

Permissions and capabilities

Use this list to declare what the operating systems ask for before the first conversation.

  • The SDK package carries no manifest. Declare the microphone yourself: Android RECORD_AUDIO and MODIFY_AUDIO_SETTINGS; iOS NSMicrophoneUsageDescription (iOS ends an app that opens the microphone without it).
  • The calls package's Android manifest merges everything a call needs — the microphone, notifications, the full-screen ring, the microphone foreground service, the call screen, the screen it asks for permissions from, its service and its receiver. On iOS, add the capabilities yourself — Push Notifications, and Background Modes → Audio and Voice over IP, plus the usage string above — or, with Expo, let the calls package's config plugin add them.
  • At run time: call requestMicrophonePermission() (below) to choose when Android asks for the bar's voice; otherwise it asks when the first call starts, and iOS asks the first time the microphone is used. For calls, GraineCalls.requestCallPermissions() asks for the microphone and, on Android 13+, notifications at a moment you choose; the calls package also asks for the microphone before it places or answers a call without it. Without notifications a ring cannot show while your app is closed. GraineCalls.callSetupProblems() names whatever is still missing (Permissions and the setup check).
  • iPhone ringer switch: the SDK leaves your app's audio session to you for the bar's voice — while a conversation is live, set your audio library's equivalents of playsInSilentModeIOS: true and allowsRecordingIOS: true, or the agent is silent when the ringer is off. Calls from the calls package set their own audio session.

JSON — app.json (Expo), with the calls package

{
  "expo": {
    "plugins": [
      ["@graineai/inapp-react-native-calls", { "microphonePermission": "Lets you talk to the assistant in the app and on calls." }]
    ]
  }
}

The plugin writes, at expo prebuild: NSMicrophoneUsageDescription (microphonePermission, else the one already in your config, else a plain default), UIBackgroundModes audio and voip beside any you have, and the Push Notifications entitlement (aps-environment development unless you set it; Xcode signs TestFlight and App Store builds with production). "webrtc": true also turns on WebRTC calls — ENV['GRAINE_WEBRTC'] = '1' in the Podfile and graineWebRtc=true in android/gradle.properties; left out, both are taken away again. A wrong option stops the prebuild with its name. Android needs nothing from it: the package's manifest merges every call entry.

JSON — app.json (Expo), the SDK package alone

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSMicrophoneUsageDescription": "Lets you talk to the assistant in the app and on calls.",
        "UIBackgroundModes": ["audio", "voip"]
      },
      "entitlements": { "aps-environment": "production" }
    },
    "android": {
      "permissions": ["android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS"]
    }
  }
}

aps-environment is the Push Notifications capability: development for development builds, production for TestFlight and App Store builds. With the calls package installed, its manifest adds the Android entries at build time anyway.

requestMicrophonePermission

Call it before the first voice conversation — on an onboarding screen, say — so the microphone prompt is not the first thing a customer sees after tapping the agent.

function requestMicrophonePermission(
  rationale: { title?: string; message?: string; buttonPositive?: string } = {}
): Promise<boolean>
ParameterTypeDefaultMeaning
rationale.titlestring"Microphone"The title of Android's explanation, shown before the system prompt when Android wants one.
rationale.messagestring"Allow the microphone so you can talk to the assistant."Why your app wants the microphone, in your words.
rationale.buttonPositivestring"Allow"The label of the explanation's button.

Returns — Promise<boolean>: true when the microphone may be used (already allowed, or allowed now), false when the customer refused. On iOS it resolves true at once — iOS asks the first time the microphone is used, and a refusal then reaches you as the mic_denied event and the launcher's onMicDenied. Example value: true.

Errors — none: it never rejects. A device whose permission service throws resolves true, so a call is still attempted.

const allowed = await requestMicrophonePermission({ message: "Talk to Acme's assistant hands-free." });
if (!allowed) showMicrophoneHelp();

Initialise

Use this as the starting point: one provider at the root, the voice engine mounted early, your own launcher button, and the calls package handed what the app tells the agent.

React Native · TypeScript — App.tsx

import { useEffect, useState } from "react";
import { WebView } from "react-native-webview";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Font from "expo-font";
import Svg, { Path } from "react-native-svg";
import { NavigationContainer, createNavigationContainerRef } from "@react-navigation/native";
import {
  GraineProvider, GraineVoiceLauncher, GraineVoiceBar, GraineLauncherButton, useGraineClient,
} from "@graineai/inapp-react-native";
import { GraineCalls } from "@graineai/inapp-react-native-calls";
 
const navigationRef = createNavigationContainerRef();
 
export default function App() {
  const [open, setOpen] = useState(false);
  return (
    <GraineProvider
      baseUrl="https://www.graine.ai"
      publishableKey="pk_live_…"            // Agent → Embed & Widgets → Install
      storage={AsyncStorage}                // keeps this install's id between launches
      navigationRef={navigationRef}         // the same ref as your NavigationContainer: screens are reported for you
      loadFont={Font.loadAsync}
      svg={{ Svg, Path }}
    >
      <NavigationContainer ref={navigationRef}><RootNavigator /></NavigationContainer>
      <GraineVoiceLauncher webView={WebView} active={open} autoStart>
        {(api) => (open ? <GraineVoiceBar api={api} onClose={() => setOpen(false)} /> : null)}
      </GraineVoiceLauncher>
      {!open && <GraineLauncherButton onPress={() => setOpen(true)} style={{ position: "absolute", right: 16, bottom: 24 }} />}
      <CallsBridge />
    </GraineProvider>
  );
}
 
// Calls that ring: hand the call what the app tells the agent.
function CallsBridge() {
  const client = useGraineClient();
  useEffect(() => GraineCalls.attach(client), [client]);   // identity, variables, the screen and your actions reach the call
  return null;
}
 
// index.js — once, at launch, before a push can arrive:
// GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Acme" });

GraineVoiceLauncher owns the voice engine and the call; mounted early with active={false} it loads the engine before the tap, and active turning true with autoStart places the call. GraineVoiceBar draws the bar, the chat window (where the agent offers chat), the components the agent draws and the booked-calls card, in the design chosen in the dashboard. RootNavigator stands for your own navigator.

WebRTC calls

Add this when the calls package's calls should carry their audio over WebRTC instead of the WebSocket — both directions: the customer calling the agent (startCall), and the customer answering a call Graine rang (a booked call, or Ring now). It is one switch per app build: your code, the ring, the call screen and the outcomes stay the same.

Ruby — ios/Podfile, at the top

ENV["GRAINE_WEBRTC"] = "1"

Properties — android/gradle.properties

graineWebRtc=true

Then run pod install in ios/ and rebuild the app. It is native code: it reaches customers in a store build, never in an over-the-air update. On a build server (EAS Build, CI), GRAINE_WEBRTC=1 in the build's environment turns it on for both platforms. Expo: a development build — with prebuild writing ios/ and android/ afresh, set "webrtc": true in the calls package's config plugin (see Permissions and capabilities), or put GRAINE_WEBRTC=1 in the build profile's environment (env in eas.json), rather than editing the two files; Expo Go cannot load the calls package.

WhatDetails
What changesCalls carry their audio over WebRTC (with callTransport at its default, "auto"): a jitter buffer, loss concealment and the platform's echo canceller, so on a weak or changing network the agent's voice holds up instead of stalling and catching up. The call is prepared at the tap — or while a ring is showing — so it connects as fast as before.
What does notYour code, the ring, the call screen, "Connecting…", the states and the outcomes. The voice bar (GraineVoiceLauncher) is untouched: this is the calls package only.
SoundOver WebRTC the agent's voice arrives at telephone bandwidth today: steadier through a weak network, narrower than the WebSocket's on a perfect one.
When WebRTC cannot set a call upBefore any audio flows — a network that blocks WebRTC, a refusal, no answer within about 12 seconds in all — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error; "Connecting…" shows a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, exactly as it does without WebRTC. Once audio flows, a call never moves to the WebSocket: a dropped network gets one retry, then the call ends as dropped.
ChoosingGraineCalls.configure({ callTransport }): "auto" (the default), "websocket" or "webrtc" — see GraineCallTransport. Which one carried a call: transport and transportFallbackReason on GraineCallSnapshot (useGraineCall()).
The dashboardThe agent's In-app call audio setting (agent → Calls & scheduling): WebRTC (recommended) or Standard. Standard moves every install's calls back to the WebSocket, whatever the app chose, from each app's next start — no app release needed.
SizeA prebuilt WebRTC library (Google's WebRTC, BSD 3-Clause, its symbols renamed so it never clashes with another copy in the app, such as react-native-webrtc). iOS: about 12 MB more installed on an arm64 iPhone, about 6 MB more to download. Android, per phone (an app bundle delivers one of these): about 7.4 MB installed on 64-bit ARM (about 2.5–3.3 MB to download), 4.3 MB on 32-bit ARM, 8.2–8.4 MB on x86 — and about 0.5 MB of classes.
PermissionsAndroid: ACCESS_NETWORK_STATE joins your manifest — a normal permission, granted at install: nothing is asked. Its R8 rules come with the package. iOS: nothing new — no Info.plist entry, no capability.
LicencesWebRTC is BSD 3-Clause, with Google's patent grant: list it in your app's open-source licences.
One WebRTC build per app (iOS)CocoaPods installs one version of the WebRTC build the calls use. If another pod in your app pins that same build at a different version, the two cannot be installed together: leave GRAINE_WEBRTC off (calls stay on the WebSocket) or align the versions.
await GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Acme", callTransport: "auto" });
const { transport, transportFallbackReason } = GraineCalls.getSnapshot();   // read once the call is active

Install and the first answer

Use these to mount the agent, build its client, load its settings and know when it is ready to answer.

GraineProvider

Wrap the root of your app in it, once per agent: it builds the agent's client (or uses yours), loads the agent's settings from the dashboard, reports the screen the customer is on, connects the chat, and gives every hook and SDK view below it the agent.

function GraineProvider(props: GraineProviderProps): JSX.Element

Props — GraineProviderProps, which also takes every GraineInAppOptions field (baseUrl, publishableKey, variables, storage, chatSize…) when you do not pass client.

Returns — the provider element. It renders children (inside tap capture when fab or captureTaps asks for it), the floating button when fab is set, and the booking sheet an openBooking() asked for.

Errors — building the client throws Error("[Graine] publishableKey is required.") or Error("[Graine] baseUrl is required.") during render when one is missing and no client is passed. Nothing else throws: a key or an agent that cannot be loaded shows as error in useGraineAgent() and in the console ([Graine] …), never as a crash.

What to expect — On mount it loads the agent's settings once (sessionReady turns true), registers its client as the default for hooks outside it (the provider at the root only), answers highlight_element for you, and — with autoConnect on — opens the chat connection, unless a GraineVoiceLauncher is mounted and the customer is on the call (the launcher holds that conversation). On unmount it closes the connection. A provider inside another provider's tree is that part of the app's agent; see Several agents in one app.

<GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_…" storage={AsyncStorage} navigationRef={navigationRef}>
  <App />
</GraineProvider>

GraineProviderProps

Read it to configure the provider: which client, when to connect, how screens are tracked, where the launcher may show, and how the design is drawn.

interface GraineProviderProps extends Partial<GraineInAppOptions> { … }

What to expect — Every prop is read on mount; appVersion, friction and navigationRef are applied again when they change. With client passed, the GraineInAppOptions fields are ignored — the client already has its own.

PropTypeDefaultMeaning
childrenReact.ReactNoderequiredYour app — everything that should reach the agent through hooks.
clientGraineInAppClientbuilt from the propsUse this client instead of building one: pass the instance your app already uses outside React (an API layer calling track, a login flow calling identify), so the provider and your code share one client.
autoConnectbooleantrueOpen the chat connection as soon as the provider mounts, so the agent can speak first. Set false to connect only when your code calls connect(). A launcher holding the call keeps the provider from opening a second conversation either way.
surfaceAgentSurfacethe dashboard's startWhich of the call ("voice") or the chat ("chat") opens first, when the dashboard allows both. A surface the dashboard does not allow is ignored.
onSurfaceChange(surface: AgentSurface) => voidnoneHear the customer move between the call and the chat — for analytics, or to change your own UI. Not called for the first surface.
onProactive(text: string) => voidnoneHear the agent speak first (a turn nobody asked for), with its first words — to draw a nudge in your own UI.
endOnBackgroundbooleantrueEnd the conversation when the app leaves the foreground (a call ends two seconds after it goes to the background) and reconnect when it returns. Set false only if a conversation must survive a switch to another app.
navigationRef{ current: any } | nullnonePass the ref you give React Navigation's NavigationContainer: every screen the customer opens is reported to the agent by its route name, and the launcher visibility rules apply per route.
appVersionstringnoneYour app's version, so each conversation's record says which build it happened in.
frictionFrictionConfignoneYour app's friction rules, layered over the dashboard's — for screens only your app knows about. See FrictionConfig.
fabOmit<GraineFabProps, "webView"> & { webView: React.ComponentType<any> }noneMount the SDK's floating button for you: pass { webView: WebView } and the button, the voice engine and the bar come with it. See GraineFabProps.
captureTapsbooleantrue with fab, else falseTrack short, still touches anywhere in your app as tap:screen (with no wrapping of each button) while a screen has been reported and the launcher may show — they feed the rage-tap rule and the conversation's timeline.
includeScreensstring[]every screenRoute names where the launcher may appear.
launcherDelayMsnumber0Wait this long after the customer lands on a screen before the launcher appears.
visibilityLauncherVisibilitynonePer-flow rules for the launcher: groups of screens, delays, continuity and insets. See LauncherVisibility.
fontsRecord<string, string>noneTypefaces your app already registered, by the family name the dashboard uses — { Inter: "Inter-Regular" } draws the design's "Inter" with your fontFamily. A family named here wins over loadFont.
loadFontFontLoadernoneLet the SDK load the design's typefaces itself: pass expo-font's Font.loadAsync. Only the faces the design draws with are fetched; text redraws when they arrive. See FontLoader.
svgSvgKitnoneLet the SDK draw its icons (microphone, hang up, send, chat, call) as vectors in the design's icon style: pass { Svg, Path } from react-native-svg. Without it, plain glyphs.

GraineInAppClient

Use it when code outside React needs the agent — a login flow, an API layer, a screen controller — and pass the instance to GraineProvider as client so both share it.

class GraineInAppClient {
  constructor(opts: GraineInAppOptions)
}
ParameterTypeDefaultMeaning
optsGraineInAppOptionsrequiredThe agent's publishable key, where Graine is, and how the client behaves — see GraineInAppOptions.

Returns — a new client, already sharing this device with the app's other clients (who the customer is, the screen, one conversation at a time).

Errors — throws Error("[Graine] publishableKey is required.") or Error("[Graine] baseUrl is required.") when one is missing. Nothing is sent until you (or the provider) ask for something.

What to expect — Every method is safe to call at any time, including before the agent's settings have loaded; a request that needs them waits for them. Its members, by use:

// agent.ts — one client for the whole app, for code outside React too
import AsyncStorage from "@react-native-async-storage/async-storage";
import { GraineInAppClient, GraineProvider } from "@graineai/inapp-react-native";
 
export const agent = new GraineInAppClient({ baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…", storage: AsyncStorage });
 
// App.tsx — the provider uses the same client (import { agent } from "./agent")
export default function App() {
  return <GraineProvider client={agent}><RootNavigator /></GraineProvider>;
}

GraineInAppOptions

Read it to build a client (or a provider without client): where Graine is, which agent, and how conversations behave on this device.

interface GraineInAppOptions { baseUrl: string; publishableKey: string; … }

What to expect — Read once, when the client is built; to change one, build a new client. Nothing here is sent to Graine except the key, the variables and the time zone, as each row says.

FieldTypeDefaultMeaning
baseUrlstringrequiredWhere Graine is: "https://www.graine.ai", or your own deployment's address. A trailing slash is ignored.
publishableKeystringrequiredThe agent's publishable key (pk_live_…, Agent → Embed & Widgets → Install). Public by design: it identifies the agent and is safe in an app bundle.
variablesRecord<string, string>nonePrompt variables your app knows at launch — the customer's first name, their plan — for the placeholders in the agent's prompt. Merged over the agent's dashboard defaults and under setVariables() and identify().
stallAfterMsnumber45000How long a customer may sit untouched on one screen before the agent is told they look stuck; 0 turns it off. The older single setting for the idle friction rule — prefer friction.
frictionFrictionConfignoneYour app's friction rules, over the built-in defaults and the dashboard's. See FrictionConfig.
voicebooleanfalseOpen this client's own connection as a spoken conversation rather than a typed chat — for an app that brings its own audio (useGraineVoice). Decided when the connection opens. Not needed for GraineVoiceLauncher.
storageGraineStorage | nullnoneWhere this install's id and each customer's memory choice are kept between launches: pass AsyncStorage as it is. Without it (and without the calls package) there is no install id, so memory, conversations that continue and booked calls refuse with invalid_request.
installIdSource() => string | null | undefined | Promise<string | null | undefined>the calls package's id, when linkedAn install id your platform already keeps, asked first. Rarely needed: with the calls package linked, the SDK uses the call engine's id, so a call and a chat are one device.
timezonestringthe device'sThe customer's IANA time zone ("Asia/Kolkata"), sent with bookings and conversations so times are read in their zone.
chatSizeChatSizethe design'sHow big the chat window opens — "compact", "large" or "full" — over the agent's Window size in the dashboard. An openChat({ size }) wins for that open.
reconnectDelaysMsnumber[][1000, 2000, 4000, 8000, 15000, 30000]How long a chat that dropped waits before each try to reconnect (each jittered by a quarter either way); the last delay repeats while the chat is wanted.
WebSocketImplanythe global WebSocketReplace the WebSocket implementation — for tests.
fetchImpltypeof fetchthe global fetchReplace fetch for every request the client makes — for tests, or a network layer of your own.

GraineStorage

Implement it only when you do not pass AsyncStorage: the client keeps this install's id and each customer's memory choice through it.

interface GraineStorage {
  getItem(key: string): string | null | undefined | Promise<string | null | undefined>;
  setItem(key: string, value: string): unknown;
  removeItem?(key: string): unknown;
}

What to expect — Synchronous or asynchronous both work. The client writes two keys, graine.installId and graine.prefs; the memory choices are stored under a one-way hash, never the customer's identifier. A storage that throws is treated as empty: the install id is then not kept.

MemberTypeDefaultMeaning
getItem(key: string) => string | null | undefined | Promise<string | null | undefined>requiredRead a value back: the install id at the first request, the memory choices once per storage.
setItem(key: string, value: string) => unknownrequiredKeep a value: a fresh install id on first launch, and the memory choices when they change.
removeItem(key: string) => unknownoptionalDelete a value. Accepted for compatibility with AsyncStorage; this release does not call it.

GraineInAppClient.opts

Read it to see the options a client was built with — its key, its address, its time zone.

readonly opts: GraineInAppOptions

Returns — the options as given, with stallAfterMs filled (45000) when you left it out. Example value: { baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…", stallAfterMs: 45000 }.

Errors — none.

const key = client.opts.publishableKey;

GraineInAppClient.session

Call it to load (or reload) the agent's settings from the dashboard — its look, its declared actions, its memory and booking rules; the provider does it once on mount.

async session(): Promise<SessionConfig>

Parameters — none.

Returns — Promise<SessionConfig>: the settings, also kept as config and announced as the session event. Every call asks Graine again. Example value: { agentId: "a_1f2e…", appearance: { accent: "#3740B8" }, calls: { hosted: true, scheduling: { usersCanBook: true } } }.

Errors — rejects with a GraineError:

  • origin_refused (403): "This agent does not accept connections from a mobile app yet. Add "app://" to its Allowed domains in the dashboard, under Embed & Widgets. (server said: …)" — add app:// to the agent's Allowed domains.
  • invalid_key (401): "Publishable key was rejected. Check it against Embed & Widgets in the dashboard."
  • any other refusal: Graine's code (rate_limited, unavailable, …) with Graine's message.

With no network at all, the platform's own fetch error (a TypeError such as "Network request failed") passes through unchanged.

try {
  const settings = await client.session();
  console.log(settings.calls?.scheduling?.usersCanBook);
} catch (e) {
  if (e instanceof GraineError && e.code === "origin_refused") showSetupHelp(e.message);
}

SessionConfig

Read it to see the agent's settings as the dashboard sets them; most apps never need to — the SDK's views and hooks read it for you.

What to expect — Loaded by session(); null on the client until then. A setting an older server does not send means its default: the user_id trait, two devices per customer, booking off. Unknown fields are kept but not read.

FieldTypeDefaultMeaning
agentIdstringrequiredThe agent's id, to tell agents apart in your own logs.
webcallAgentIdstringoptionalThe id of the agent's voice counterpart, when it has one; voice conversations use it.
wsHoststringoptionalThe host a direct conversation connects to, when Graine sends one.
appearanceRecord<string, any>optionalThe agent's Appearance settings exactly as saved — accent, heading, logo, radius, light or dark, loader. Read it through resolveNativeAppearance() rather than raw.
designResolvedDesignoptionalThe agent's design and its tokens for light and dark, as Graine resolved them. Absent from an older server; read it through sessionDesign(), which fills anything missing.
variablesRecord<string, string>optionalThe agent's default prompt variables from the dashboard; your own variables are merged over them.
componentsany[]optionalThe components the agent may draw, as configured in the dashboard. The SDK's views draw them; you rarely read this.
appActionsArray<{ tool_name?: string; toolName?: string; name?: string }>optionalThe in-app actions the agent is allowed to call. Only actions declared here and registered in your app work — see checkActions().
frictionFrictionConfigoptionalThe friction rules set in the dashboard, applied under your app's own.
pageVersionstringoptionalWhich version of the voice engine Graine serves, so the launcher reloads it only when it changes.
identity{ field?: string; devicesPerUser?: number }optionalWho the customer is, as the agent's Memory section says: the trait to key on (user_id when absent) and how many devices one customer may use (2 when absent).
memory{ enabled?: boolean }optionalSays whether this agent keeps any memory of customers.
calls{ hosted?: boolean; platforms?: string[]; callName?: string; ringTimeoutSeconds?: number; scheduling?: { usersCanBook?: boolean; maxDaysAhead?: number; minLeadMinutes?: number; maxPendingPerUser?: number } }optionalCalls Graine rings and what customers may book: whether Graine rings this workspace's apps (hosted), on which platforms, the name the ring shows (callName, also the booking sheet's "… will call you"), how long it rings, and the booking limits. Read it through schedulingRules and bookingAvailable.

GraineInAppClient.config

Read it to see the agent's settings once they have loaded, without asking Graine again.

config: SessionConfig | null

Returns — the SessionConfig the last session() loaded, or null before one has. Example value: null at launch, then { agentId: "a_1f2e…", … }.

Errors — none.

const settings = client.config ?? (await client.session());

GraineInAppClient.connect

Call it to open this client's conversation connection — a typed chat, or a spoken one with voice: true — when autoConnect is off or after you closed it.

async connect(options: { automatic?: boolean } = {}): Promise<void>
ParameterTypeDefaultMeaning
options.automaticbooleanfalsetrue for a connection nobody asked for this moment (launch, back from the background): it ends another agent's chat on this device but never its call — it waits for the call to end instead, resolving at once without connecting.

Returns — Promise<void>, resolved once the connection is open (or at once when it is already open, opening, or waiting for another agent's call). The connection is announced as the open event; the agent's welcome follows.

Errors — rejects when the agent's settings cannot load (a GraineError as session() raises), with Error("Socket did not open in time") after 15 seconds, or with the connection's own error. A first connection that fails is not retried — fix what it says; the chat then says it gave up (chatFailed, the chat_failed event), and retry() tries again. A chat that had opened and then dropped comes back by itself (the reconnecting event).

What to expect — One conversation at a time on the device: connecting ends a chat another agent's client holds, and (unless automatic) its call. Variables and the identity are fixed when the connection opens.

await client.connect();
client.sendText("Where is my order?");

GraineInAppClient.isConnected

Read it to know whether this client holds an open conversation connection right now.

get isConnected(): boolean

Returns — true while this client's own connection is open. A call the voice launcher holds is not this client's connection: during it, this is usually false. Example value: true.

Errors — none.

if (!client.isConnected) await client.connect();

GraineInAppClient.transport

Read it to see how the current conversation connected, when you are diagnosing a connection.

transport: "gateway" | "direct" | null

Returns — "gateway": the usual connection, opened with a single-use ticket Graine issued for it — your variables travel in the ticket (see TICKET_VARIABLE_LIMITS); "direct": no ticket could be had (a deployment that issues none), so the client connected directly and the variables travel in the connection's address (see contextToken); null before the first connection. Example value: "gateway".

Errors — none.

client.on("open", ({ transport }) => console.log("connected over", transport));

GraineInAppClient.close

Call it to hang up this client's conversation and stop it reconnecting — when the customer leaves the agent, or your app is done with it.

close(): void

Parameters — none.

Returns — nothing. The agent is told the customer hung up (so the conversation is recorded as ended, not dropped), then the connection closes a moment later; the close event follows with byClient: true.

Errors — none: closing a connection that is already gone does nothing.

client.close();

useGraineAgent

Call it in any component under the provider to draw the conversation: its state, the transcript, the controls, the design, and whether booking is available.

function useGraineAgent(): GraineContextValue

Parameters — none.

Returns — the provider's GraineContextValue, re-rendering as it changes.

Errors — throws Error("[Graine] useGraine* must be used inside <GraineProvider>.") outside a provider.

const { messages, send, typing, connected, failed, retry, bookingAvailable, openBooking } = useGraineAgent();

GraineContextValue

Read it from useGraineAgent() to draw your own conversation UI; the type is not exported by name — write ReturnType<typeof useGraineAgent>.

What to expect — Updated as the conversation moves; every field is state, so draw from it. For things you do once when they happen (log, analytics), subscribe with useGraineEvents.

FieldTypeDefaultMeaning
clientGraineInAppClientrequiredThe provider's client, for anything the hooks do not cover.
connectedbooleanrequiredWhether the chat connection is open — draw "online" from it.
connectingbooleanrequiredWhether a connection is being opened — draw a spinner from it.
errorstring | nullrequiredWhy the automatic connection failed (a rejected key, an agent not open to apps), else null — show it to your team, not the customer.
openbooleanrequiredWhether your agent panel should be open; the provider opens it when the agent speaks first.
setOpen(open: boolean) => voidrequiredOpen or close your agent panel.
messagesTurn[]requiredThe transcript, oldest first — one entry per message, the agent's turn growing in place while it streams. See Turn.
widgetsany[]requiredEvery component the agent has drawn in this provider's lifetime, oldest first. To draw the current one, use useGraineWidget().
send(text: string) => voidrequiredSend what the customer typed. Never lost: with no connection it shows as waiting (pending) and goes the moment there is one.
typingbooleanrequiredThe agent has the message but not one word of its reply has come back yet — show a typing indicator meanwhile. Ends with the first word, a component, the connection closing, or after 12 seconds.
reconnectingbooleanrequiredThe chat dropped and is coming back by itself — say "Reconnecting…" rather than "Offline".
failedbooleanrequiredThe chat gave up: its first connect failed, or Graine ended or refused the conversation — it will not come back by itself. Say so and offer retry(); a message typed now waits (pending), and sending it tries again too. A dropped network is never failed: it stays reconnecting. See chatFailed.
retry() => voidrequiredConnect the chat again now — after it gave up, or to cut a reconnect's wait short; connecting is true meanwhile. See retry.
clearChat() => voidrequiredEmpty the chat on screen. Only the screen: the conversation goes on, a message still waiting is still sent, and nothing Graine keeps is touched. See clearChat.
mutedbooleanrequiredWhether this client's own voice path (useGraineVoice) is holding the customer's voice back. A call the voice launcher holds has its own muted on GraineVoiceApi.
setMuted(muted: boolean) => voidrequiredMute or unmute the customer on this client's own voice path; for the voice launcher's call, use its api.setMuted.
agentSpeakingbooleanrequiredWhether the agent is mid-reply — drive a speaking animation from it.
captionCaption | nullrequiredThe line being said right now, by either side — for live captions.
launcherVisiblebooleanrequiredWhether your launcher may be on screen now, after the route rules and delays.
launcherInsetLauncherInsetrequiredWhere your launcher should sit on this screen, clear of tab bars and sheets.
currentScreenstring | nullrequiredThe route the SDK believes the customer is on (with navigationRef), else null.
appearanceRecord<string, any> | nullrequiredThe agent's Appearance settings as saved in the dashboard; null until they load. Read through resolveNativeAppearance().
designResolvedDesignrequiredThe agent's design for light and dark — always complete (the default design until the settings load). To draw with it, use useGraineTheme(): it chooses the right scheme for you.
fontsRecord<string, string> | nullrequiredThe provider's fonts, or null.
facesLoadedFacesrequiredThe design's typefaces the SDK has loaded through loadFont, as "Family|weight" → the fontFamily registered. Empty until one loads.
svgSvgKit | nullrequiredThe provider's svg, or null.
endOnBackgroundbooleanrequiredThe provider's endOnBackground, which the voice launcher follows too.
sessionReadybooleanrequiredWhether the agent's settings have loaded — show a splash until it is true.
onEvent(fn: (e: GraineEvent) => void) => () => voidrequiredSubscribe to the event stream; returns the unsubscribe. useGraineEvents wraps it.
track(name: string, data?: Record<string, unknown>) => voidrequiredRecord a product event the agent should know about, without making it speak.
identify(user: Record<string, unknown> | null, options?: { memory?: boolean }) => voidrequiredTell the agent who the customer is; null signs out. See identify.
newConversation() => Promise<void>requiredStart a fresh conversation. See newConversation.
surfaceAgentSurfacerequiredWhich conversation the customer is in: the call ("voice") or the chat ("chat").
surfacesAgentSurface[]requiredEach surface the dashboard permits, listed in the order you should offer them.
canSwitchbooleanrequiredWhether to draw a call/chat switch at all.
setSurface(surface: AgentSurface) => voidrequiredMove the customer between the call and the chat; ends the one they leave. A surface the dashboard does not allow is refused.
chatSizeChatSizerequiredHow big the chat window opens now: the last openChat() size, else the provider's chatSize, else the design's.
openChat(options?: { size?: ChatSize }) => voidrequiredMove to the chat and open it. See openChat.
endChatOpen() => voidrequiredFor the SDK's own bar: the chat window closed, so an open's size no longer applies.
chatScreenShownbooleanrequiredWhether a GraineChatScreen is on screen (the bar's own chat steps aside while it is).
noteChatScreen(delta: number) => voidrequiredFor GraineChatScreen only: counts chat screens appearing (+1) and going (−1).
connectionQualityConnectionQualityrequiredThe connection in one word — excellent, good, poor, lost, or unknown outside a call.
network"online" | "offline" | "unknown"requiredThe SDK's most recent reading of whether the device is online.
setVariables(vars: Record<string, unknown> | null) => voidrequiredSet prompt variables while the app runs. See setVariables.
bookingAvailablebooleanrequired0.42.0. Whether this customer can book a call from here — show or hide your own "Call me" button with it. Re-read when the settings load and when the customer changes. See bookingAvailable.
openBooking(options?: { reason?: string }) => Promise<GraineScheduledCall | null>required0.42.0. Open the SDK's booking sheet, which this provider draws; resolves with the booked call, or null when it closed without one. See openBooking.

useGraineClient

Call it in a component to get the agent's client — the provider's, or the default one registered with setDefaultClient when no provider is above.

function useGraineClient(): GraineInAppClient

Parameters — none.

Returns — the GraineInAppClient for this render.

Errors — throws Error("[Graine] No client. Either render inside <GraineProvider>, or call setDefaultClient(client) once at startup.") when there is neither.

const client = useGraineClient();
useEffect(() => client.registerAction("open_cart", () => { openCart(); return { status: "ok" }; }), [client]);

setDefaultClient

Call it once at startup when some of your screens sit outside the provider (a provider that wraps only your bar), so the SDK's hooks on those screens still reach the agent.

function setDefaultClient(client: GraineInAppClient | null): void
ParameterTypeDefaultMeaning
clientGraineInAppClient | nullrequiredThe client hooks use where no GraineProvider is above them; null clears it.

Returns — nothing.

Errors — none.

What to expect — A provider at the root registers its own client this way on mount, and hands the default back when it unmounts; a provider around one screen does not.

setDefaultClient(agent);

useGraineReady

Call it to show a splash, or a set-up error, until the agent's settings have loaded.

function useGraineReady(): { ready: boolean; error: string | null }

Parameters — none.

Returns — ready: true once the agent's settings have loaded; error: why the automatic connection failed (a rejected key, an app not allowed), else null. Example value: { ready: true, error: null }.

Errors — throws outside a provider, as useGraineAgent does.

const { ready, error } = useGraineReady();
if (error) return <SetupError message={error} />;
if (!ready) return <Splash />;

useGraineInit

Call it as useGraineReady, when you prefer the isInitialized name.

function useGraineInit(): { isInitialized: boolean; error: string | null }

Parameters — none.

Returns — isInitialized: true once the agent's settings have loaded; error: as useGraineReady. Example value: { isInitialized: false, error: null }.

Errors — throws outside a provider, as useGraineAgent does.

const { isInitialized } = useGraineInit();

Identity, memory and conversations that continue

Use these to tell the agent who the customer is, let them choose whether they are remembered, show and forget what is remembered, and start a fresh conversation.

GraineInAppClient.identify

Call it after sign-in and on every account switch — and with null on sign-out — so the agent greets the customer by name, remembers them, and rings their devices for calls they book.

identify(user: Record<string, unknown> | null, options: { memory?: boolean } = {}): void
ParameterTypeDefaultMeaning
userRecord<string, unknown> | nullrequiredThe customer's traits. The trait your agent's Who is the user? setting names (user_id unless you chose another) identifies them for memory, their devices and booked calls, and never reaches the prompt. Every other trait becomes a prompt variable, masked on the device. null signs out.
options.memorybooleantheir last choice (true at first)false keeps this customer's conversations out of memory — neither remembered nor recalled; true turns it back on. Kept for this customer on this install, so you need not repeat it.

Returns — nothing, at once; the sign-out of a previous customer goes on in the background.

Errors — none: it never throws. A sign-out that cannot reach Graine is logged, and the device is released anyway after 30 days without use.

What to expect —

  • Values are turned into strings (an object becomes its JSON); null and undefined traits are skipped.
  • The identifying value counts as nobody when it is empty or a placeholder — guest, anonymous, unknown, null, none, 0, -, n/a, test, or a masked token such as [EMAIL]. An email trait is compared lowercased and must look like an address; a phone trait keeps its last 10 digits (8 to 15 digits in all); any other trait is exact, at most 256 characters.
  • The identity applies from the next conversation: a conversation's customer is fixed when it starts. Prompt variables reach a live call at once.
  • Signing in someone else signs the previous customer out first and clears the chat on screen. identify(null) releases this device from the customer — it stops ringing for them and stops counting as one of their devices — and clears the chat.
  • Every app on React Native shares one device across its agents: one identify() reaches every agent's client.
const identify = useGraineIdentify();
useEffect(() => {
  identify(user ? { user_id: user.id, name: user.firstName, plan: user.plan } : null, { memory: user?.allowsMemory ?? true });
}, [user]);
// outside React: client.identify({ user_id: "u_8812", name: "Asha" }); client.identify(null);

useGraineIdentify

Call it in a component to get identify bound to the agent's client — sign-in and sign-out effects are the usual place.

function useGraineIdentify(): (user: Record<string, unknown> | null, options?: { memory?: boolean }) => void

Parameters — none.

Returns — a function with the same parameters and behaviour as GraineInAppClient.identify.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const identify = useGraineIdentify();
const signOut = () => { identify(null); auth.signOut(); };

GraineInAppClient.setMemory

Call it from a privacy setting — "Let the assistant remember our conversations" — to change the current customer's memory choice without identifying them again.

setMemory(enabled: boolean): void
ParameterTypeDefaultMeaning
enabledbooleanrequiredtrue to remember this customer's conversations; false to keep them out of memory.

Returns — nothing. The choice is kept for this customer on this install (under a one-way hash, never their identifier) and announced as the identity event.

Errors — none.

What to expect — Applies to every agent on the device, from the next conversation. Call it while a customer is identified: with nobody identified, the choice lasts only until the next identify().

client.setMemory(false);

GraineInAppClient.memoryEnabled

Read it to draw the current customer's memory switch in its true state.

get memoryEnabled(): boolean

Returns — true unless setMemory(false) or identify(…, { memory: false }) said otherwise for this customer on this install. Example value: true.

Errors — none.

<Switch value={client.memoryEnabled} onValueChange={(on) => client.setMemory(on)} />

GraineInAppClient.getIdentity

Read it to see the traits the agent will receive as prompt variables — masked, and without the identifying trait.

getIdentity(): Record<string, string>

Parameters — none.

Returns — a copy of the masked traits from the last identify(), minus the trait that identifies the customer. Example value: { name: "Asha", plan: "Plus" }.

Errors — none.

console.log(client.getIdentity());

GraineInAppClient.getRawTraits

Read it only when you hand the customer to an engine that masks traits itself — the calls package's attach() does this for you.

getRawTraits(): Record<string, unknown>

Parameters — none.

Returns — a copy of the traits exactly as identify() was given them, unmasked. This client never sends them anywhere. Example value: { user_id: "u_8812", name: "Asha", plan: "Plus" }.

Errors — none.

const traits = client.getRawTraits();

GraineInAppClient.installId

Read it to see this install's id — the id that makes this phone one of the customer's devices — once it has been read.

get installId(): string | null

Returns — a lowercase UUID made on first launch and kept, or null until it has been read (any request reads it; so does getInstallId()), and null for good when it cannot be kept — pass storage. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none.

const id = client.installId ?? (await client.getInstallId());

GraineInAppClient.getInstallId

Call it to read this install's id, making it on first launch when it has to.

async getInstallId(): Promise<string | null>

Parameters — none.

Returns — Promise<string | null>: the calls package's id when it is linked (so a call and a chat are one device), else the id kept in storage (made and kept on first launch), else null. With neither, the console warns once that the device has no install id — "so the agent cannot remember this customer or call them back" — and names the fixes: storage={AsyncStorage} on the provider or the client, or the calls package. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none: a storage that fails resolves null.

const id = await client.getInstallId();

GraineInAppClient.identityChannel

Call it when you carry the conversation yourself and must tell Graine who it is with: the customer's identifying value, this install, the memory choice and the time zone.

async identityChannel(): Promise<IdentityChannel>

Parameters — none.

Returns — Promise<IdentityChannel>, its fields in IdentityChannel. It changes with identify(), setMemory() and the agent's settings, and each change is announced as the identity event. Example value: { value: "u_8812", installId: "9b2e7c4a-…", memory: true, timezone: "Asia/Kolkata" }.

Errors — none.

const who = await client.identityChannel();

IdentityChannel

Read it to see who a conversation is with, as the SDK hands it to whatever carries the conversation.

What to expect — value is null until the agent's settings have loaded (the identifying trait is never guessed) and whenever nobody is identified.

FieldTypeDefaultMeaning
valuestring | nullrequiredThe raw value of the trait that identifies the customer, or null when nobody is identified — what memory, devices and booked calls are keyed on.
installIdstring | nullrequiredThis install's id, or null when none can be kept.
memorybooleanrequiredWhether this customer's conversations may be remembered (true unless they said no).
timezonestring | nullrequiredThe customer's IANA time zone (opts.timezone, else the device's), or null when the device cannot say.

DEFAULT_IDENTITY_FIELD

Read it to know which trait identifies the customer when the agent's Who is the user? setting names none.

const DEFAULT_IDENTITY_FIELD = "user_id";

What to expect — Always "user_id". Pass the customer's id under this name in identify() unless the agent names another trait (email, phone, …).

GraineInAppClient.memories

Call it on a "What the assistant remembers" screen to show the customer what the agent keeps about them.

async memories(): Promise<GraineMemory[]>

Parameters — none.

Returns — Promise<GraineMemory[]>: newest first, at most 100, already redacted. See GraineMemory. Example value: [{ id: "m_41", text: "Prefers a call after 6 pm", createdAt: new Date("2026-10-02T09:14:00Z"), updatedAt: new Date("2026-10-12T17:40:00Z") }].

Errors — rejects with a GraineError:

  • identity_required — "Nobody is identified. Call identify() with the customer's id first."
  • invalid_request — the device has no install id (the message names storage and the calls package).
  • device_limit — this install is not one of the customer's devices.
  • rate_limited, unavailable, invalid_key, origin_refused — from Graine; network — "Could not reach Graine. Check the connection and try again."
try {
  setItems(await client.memories());
} catch (e) {
  if (e instanceof GraineError && e.code === "identity_required") promptSignIn();
}

useGraineMemories

Call it in a component that lists what the agent remembers about the customer — it loads, reloads and reports errors as state.

function useGraineMemories(): { memories: GraineMemory[]; loading: boolean; error: GraineError | null; refresh: () => Promise<void> }

Parameters — none.

Returns — memories: the list, newest first (empty on error); loading: a read is in progress; error: the last read's GraineError — identity_required before sign-in, device_limit on a device that is not one of theirs — else null; refresh(): read again (it never rejects; failures land in error). It reads on mount, when the customer changes, and after forgetMemory(). Example value: { memories: [], loading: false, error: null, refresh: [Function] }.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const { memories, loading, error, refresh } = useGraineMemories();
if (loading) return <Spinner />;
return <FlatList data={memories} keyExtractor={(m) => m.id} renderItem={({ item }) => <Text>{item.text}</Text>} />;

GraineMemory

Read it to show one thing the agent remembers about the customer.

What to expect — From memories(): newest first, at most 100, already redacted, only for the identified customer on one of their devices. The dates are moments in time — show them in the device's time zone; null when Graine did not send one.

FieldTypeDefaultMeaning
idstringrequiredThe memory's id, as a list key.
textstringrequiredWhat the agent remembers, in one sentence, already redacted — show it as it is.
createdAtDate | nullrequiredWhen the agent first remembered it.
updatedAtDate | nullrequiredWhen it last changed.

GraineInAppClient.forgetMemory

Call it from a "Forget everything" control to delete what this agent remembers about the current customer.

async forgetMemory(): Promise<number>

Parameters — none.

Returns — Promise<number>: how many memories were deleted. The chat on screen clears too (the conversation it held is part of what was forgotten), messages still waiting are dropped, and memories_changed is announced so lists reload. Example value: 12.

Errors — rejects with a GraineError, as memories does: identity_required, invalid_request (no install id), device_limit, rate_limited, network, unavailable, invalid_key, origin_refused. On a failure nothing is forgotten and the chat stays.

const deleted = await client.forgetMemory();
showToast(`Forgot ${deleted} things`);

GraineInAppClient.newConversation

Call it from a "New conversation" control to start a fresh thread, as a support chat offers one; the chat window's header does this for you.

async newConversation(): Promise<void>

Parameters — none.

Returns — Promise<void>, once the fresh conversation has started. The chat hangs up; Graine lets go of the conversation it kept for this agent and customer, so the next chat starts afresh; the chat on screen clears; and — when the customer was in the chat — a new chat opens with the welcome. What the agent remembers (memories) is untouched. With nobody identified there was nothing kept, so only the screen clears.

Errors — rejects with a GraineError when Graine refused (device_limit, invalid_request for a missing install id, network, rate_limited, unavailable); the chat is then reconnected as it was. A call in progress is not a chat and is left alone.

await client.newConversation();

RestoredTurn

Read it to understand the earlier messages a continuing conversation opens with; the SDK's chat folds them in for you.

What to expect — For an identified customer, on an agent with memory on, on one of their devices, the chat opens with the earlier conversation — up to 50 messages from the last 7 days, oldest first — before any new message, and with no welcome. They arrive as history, become ChatTurns with restored: true, and are never live.

FieldTypeDefaultMeaning
idstringrequiredGraine's id for the message — "h<index>" when the frame had none or repeated one — so a list keeps its place.
role"agent" | "customer"requiredWho said it — draw the bubble on that side.
textstringrequiredWhat was said, never blank.
atnumberrequiredWhen it was said, in milliseconds since 1970 (UTC) — for the day separators.
channel"chat" | "voice"required"voice" for something said on a call — a chat window draws a run of these as one "Voice call" row — else "chat".

Chat

Use these to open the typed conversation, send what the customer types, move between the call and the chat, and draw a chat window of your own from the same rules the SDK's windows use.

GraineInAppClient.openChat

Call it from your own "Message us" button to move the customer to the chat and open it — at a size of your choosing for this open.

openChat(options?: { size?: ChatSize }): void
ParameterTypeDefaultMeaning
options.sizeChatSizethe app's chatSize, else the design's Window sizeHow big the chat opens this time: "compact", "large" or "full". A word this release does not know is passed over.

Returns — nothing. A GraineProvider's bar moves to the chat (where the agent offers one), draws it at that size and turns open on; a provider mounted later still answers it. Announced as the open_chat event ({ size }).

Errors — none.

client.openChat({ size: "full" });

GraineInAppClient.takeChatOpen

The provider calls it to answer an openChat() exactly once; your app does not need it.

takeChatOpen(): { size: ChatSize | null } | null

Parameters — none.

Returns — the open still waiting to be answered — { size }, size null when the open named none — and clears it; null when there is none. Example value: { size: "large" }.

Errors — none.

const pending = client.takeChatOpen();

GraineChatScreen

Use it when the chat should be a screen of your own app — a Help route — rather than a window over it: the header with a back chevron, the conversation, the components the agent draws, the booked calls and the composer, in the agent's design.

function GraineChatScreen(props: GraineChatScreenProps): JSX.Element

Props — GraineChatScreenProps.

Returns — the chat, filling its parent.

Errors — throws outside a GraineProvider, as useGraineAgent does.

What to expect — While it is on screen the customer is in the chat (where the agent offers one) and the bar's own chat steps aside; when it goes, what was open before comes back. Since 0.42.0 it shows the customer's booked calls — an Upcoming calls card with up to three calls, each with Cancel — and Book a call where booking is available, which opens the booking sheet over it (see Booking with the SDK's sheet). The header offers New conversation while an earlier conversation is on screen.

function HelpScreen({ navigation }: { navigation: { goBack(): void } }) {
  const insets = useSafeAreaInsets();
  return <GraineChatScreen onClose={navigation.goBack} safeArea={insets} />;
}
// <Stack.Screen name="Help" component={HelpScreen} />

GraineChatScreenProps

Read it to wire GraineChatScreen into your navigation.

PropTypeDefaultMeaning
onClose() => voidnoneWhat the header's back chevron does — navigation.goBack, say. Without it no chevron is drawn.
safeArea{ top?: number; bottom?: number }a typical phone'sThe phone's safe areas, so the header and the composer clear the notch and the home indicator — pass react-native-safe-area-context's useSafeAreaInsets().

GraineInAppClient.sendText

Call it with what the customer typed in a chat you draw yourself: it is sent now when there is a connection, and kept in order until there is one when there is not.

sendText(text: string): { id: string; queued: boolean }
ParameterTypeDefaultMeaning
textstringrequiredThe customer's message, as they typed it.

Returns — id: this message's id ("t1", "t2", …), to match it to its text_sent event; queued: false when it went now — on this client's connection, or to the call the voice launcher holds (the agent takes a typed turn on a call) — and true when it is waiting for a connection. Example value: { id: "t3", queued: true }.

Errors — none: a message is never dropped in silence. At most 20 wait; beyond that the oldest is dropped.

const { id, queued } = client.sendText("Where is my order?");
if (queued) markWaiting(id);
client.on("text_sent", (e) => markSent(e.id));

GraineInAppClient.queuedTexts

Read it to show the messages still waiting for a connection.

get queuedTexts(): Array<{ id: string; text: string }>

Returns — a copy of the waiting messages, oldest first; empty when everything has gone. Example value: [{ id: "t3", text: "Where is my order?" }].

Errors — none.

const waiting = client.queuedTexts.length;

GraineInAppClient.say

Use it only to send text on this client's own open connection right now — for what the customer typed, use sendText, which never loses a message.

say(text: string): boolean
ParameterTypeDefaultMeaning
textstringrequiredThe text to send as the customer's turn.

Returns — true when it went on this client's connection; false when there is none open — nothing is kept. Replies arrive as chunk events. Example value: false.

Errors — none.

if (!client.say("Yes, please")) client.sendText("Yes, please");

GraineInAppClient.chatFailed

Read it to know the chat gave up — so you can say so and offer to try again; useGraineAgent().failed is the same, as state.

get chatFailed(): boolean

Returns — true while the chat is still wanted (connect() and no close() since) yet is not connected, not connecting and not waiting to reconnect: its first connect failed — a setting to fix, never retried by itself — or Graine ended or refused the conversation. false again the moment someone asks again (retry(), connect(), or the customer's next message through useGraineAgent().send), when the chat opens, and when it is closed. Announced as chat_failed, with true and then false. Example value: false.

Errors — none.

What to expect — A dropped network is never chatFailed: the chat keeps coming back by itself (reconnecting) for as long as the customer is in it. A message typed meanwhile waits, pending, and goes the moment the chat connects. A spoken connection (voice: true) never reports it.

client.on("chat_failed", (gaveUp) => { if (gaveUp) showRetry(() => client.retry()); });

GraineInAppClient.retry

Call it from a "Try again" button once the chat gave up (chatFailed) — or to cut a reconnect's wait short.

retry(): Promise<void>

Parameters — none.

Returns — Promise<void>, as connect returns it: resolved once the chat is open. The wait between tries starts over from the first of reconnectDelaysMs, and what the customer typed meanwhile goes the moment the chat connects. A person asked, so — as connect() does — it ends another agent's conversation on this device. useGraineAgent().retry() does the same and turns connecting on meanwhile.

Errors — rejects as connect() does. A chat that never opened is then chatFailed again; one that had opened keeps trying by itself.

await client.retry();

GraineInAppClient.clearChat

Call it from a "Clear" control to empty the chat on screen — a new topic, a tidy screen — without ending the conversation.

clearChat(): void

Parameters — none.

Returns — nothing. Announced as chat_cleared with reason: "app", which empties useGraineAgent().messages and the SDK's chat windows (foldChat folds it as cleared). useGraineAgent().clearChat() is the same.

Errors — none.

What to expect — Only the screen: the conversation goes on (connected stays connected), a message still waiting for the connection is still sent, and nothing Graine keeps is touched — the conversation that continues comes back with the next history, and the agent's memories stay. newConversation starts over; forgetMemory forgets.

const { clearChat } = useGraineAgent();
<Button title="Clear" onPress={clearChat} />

GraineInAppClient.inTextChat

Read it to know whether the conversation right now is a typed chat — to word a component's prompt as "tap" rather than "say".

get inTextChat(): boolean

Returns — true when the customer is on the chat surface, or this client's own connection is open as a typed chat. Example value: true.

Errors — none.

const hint = client.inTextChat ? "Tap one" : "Tap one, or just say it";

useGraineSurface

Call it to draw a call/chat switch, or to know which of the two the customer is in — from a component that is not the transcript.

function useGraineSurface(): { surface: AgentSurface; surfaces: AgentSurface[]; canSwitch: boolean; setSurface: (surface: AgentSurface) => void }

Parameters — none.

Returns — surface: the conversation the customer is in ("voice" or "chat"); surfaces: what the dashboard allows, in the order to offer them; canSwitch: whether to draw a switch at all; setSurface: move the customer — it ends the conversation they leave, and refuses (and logs) a surface the dashboard did not allow. Example value: { surface: "chat", surfaces: ["voice", "chat"], canSwitch: true, setSurface: [Function] }.

Errors — throws outside a GraineProvider.

const { surface, canSwitch, setSurface } = useGraineSurface();
{canSwitch && <Button title={surface === "voice" ? "Type instead" : "Talk instead"} onPress={() => setSurface(surface === "voice" ? "chat" : "voice")} />}

GraineInAppClient.surface

Read it outside React to know whether the customer is in the call or the chat — an analytics wrapper, a screen controller.

get surface(): AgentSurface

Returns — "voice" or "chat". The provider keeps it in step with the dashboard's policy and the customer's choice; a client with no provider stays "voice". Example value: "voice".

Errors — none.

analytics.log("agent_opened", { surface: client.surface });

GraineInAppClient.setSurface

The provider calls it to record which conversation the customer is in; to move the customer, use setSurface from useGraineSurface, which also ends the conversation they leave.

setSurface(next: AgentSurface): void
ParameterTypeDefaultMeaning
nextAgentSurfacerequired"voice" or "chat"; anything else is ignored.

Returns — nothing. A change is announced as the surface event ({ surface }).

Errors — none.

client.setSurface("chat");

AgentSurface

Read it to tell the two conversations apart: the call or the chat — one at a time, never both.

type AgentSurface = "voice" | "chat";
ValueMeaning
"voice"The call: the customer talks and the agent speaks, in the voice bar.
"chat"The typed chat: silent, turn by turn.

SurfacePolicy

Read it to see which conversations the dashboard allows and which one opens first (Embed & Widgets → What visitors get).

FieldTypeDefaultMeaning
allowedAgentSurface[]requiredThe surfaces this agent may offer, in the order to offer them.
startAgentSurfacerequiredThe surface that opens first.
canSwitchbooleanrequiredWhether switching between them is open to the customer; false if just one is allowed.

resolveSurface

Use it to read the dashboard's call/chat policy yourself — the provider already does.

function resolveSurface(appearance?: Record<string, any> | null, prefer?: AgentSurface | null): SurfacePolicy
ParameterTypeDefaultMeaning
appearanceRecord<string, any> | nullnoneThe agent's Appearance settings (useGraineAgent().appearance): its mode ("voice", "chat" or both) and start.
preferAgentSurface | nullnoneYour app's choice of what opens first. It wins only among the surfaces the dashboard allows — it can never add one.

Returns — a SurfacePolicy. An absent or unreadable setting allows both, voice first. Example value: { allowed: ["voice", "chat"], start: "chat", canSwitch: true }.

Errors — none.

const { appearance } = useGraineAgent();
const policy = resolveSurface(appearance, "chat");

Turn

Read it to draw one message of the provider's transcript (useGraineAgent().messages).

What to expect — One per message, oldest first. The agent's turn grows in place while it streams (live), then stops. A message typed with no connection is pending until it goes. Earlier messages of a conversation that continues arrive once, restored, before anything new. Consecutive voice turns are drawn as one "Voice call" row. The list empties on sign-out, when another customer signs in, after forgetMemory() and on newConversation(); at most 200 are kept.

FieldTypeDefaultMeaning
role"agent" | "customer"requiredWho said it — draw the bubble on that side.
textstringrequiredWhat was said so far (the agent's text grows while live).
livebooleanoptionaltrue while the agent is still streaming this turn — draw it as provisional.
pendingbooleanoptionaltrue while the customer's message waits for a connection — draw it as "sending".
idstringoptionalMatches a waiting message to its text_sent; a restored turn's id is Graine's.
proactivebooleanoptionaltrue when the agent started this turn unasked — the welcome, or the agent speaking up after an app event.
atnumberoptionalWhen it was said, in milliseconds since 1970 — for timestamps and day separators.
restoredtrueoptionalFrom an earlier conversation, delivered before this one — not said just now.
channel"voice"optionalSaid on a call; absent for the chat.

ChatTurn

Read it to draw one message when you assemble the chat yourself with foldChat; it is the same shape as Turn.

What to expect — As for Turn: one per message, oldest first, the agent's growing in place while live; at most CHAT_HISTORY_CAP (200) are kept.

FieldTypeDefaultMeaning
role"agent" | "customer"requiredWho said it.
textstringrequiredWhat was said so far.
livebooleanoptionalThe agent is still streaming this turn.
pendingbooleanoptionalSet while the customer's message waits for a connection; it is sent as soon as one is available.
idstringoptionalTies a waiting message to its customer_sent event; a restored turn carries Graine's id.
proactivebooleanoptionalThe agent started this turn without being asked.
atnumberoptionalWhen it was said, in milliseconds since 1970, when the event that began it carried a time.
restoredtrueoptionalRestored from an earlier conversation — delivered, not said just now.
channel"voice"optionalSaid on a call; a chat window draws a run of these as one "Voice call" row.

ChatState

Read it to draw a chat you assemble yourself: the turns, and whether to show "typing".

FieldTypeDefaultMeaning
turnsChatTurn[]requiredThe messages, oldest first.
typingbooleanrequiredA message reached the agent and no word of the reply has arrived — draw a typing indicator.
awaitingReplybooleanrequiredA message of the customer's reached the agent and no agent turn has started since; the next agent turn is then not proactive.
settledReadonlySet<ChatTurn>optionalThe turns already delivered when the connection last opened — what a history frame replaces. Absent until a connection has opened.

ChatEvent

Read it to feed foldChat: each event is one thing that happened to the chat, branched on type.

type ChatEvent =
  | { type: "agent_text"; text: string; at?: number }
  | { type: "agent_turn_start" } | { type: "agent_turn_end" }
  | { type: "customer"; text: string; id?: string; queued?: boolean; at?: number }
  | { type: "customer_sent"; id: string }
  | { type: "widget" } | { type: "closed" } | { type: "typing_timeout" } | { type: "opened" }
  | { type: "history"; turns: RestoredTurn[] } | { type: "cleared" };
typeFieldsMeaning
agent_texttext, at?Words of the agent's current turn: appended to it, or the start of a new turn when none is open.
agent_turn_startnoneAn agent turn begins; whatever was open is finished.
agent_turn_endnoneThe agent's turn is finished.
customertext, id?, queued?, at?The customer sent a message; queued: true when it is waiting for a connection.
customer_sentidA waiting message went out; its turn stops being pending.
widgetnoneThe agent answered with a component; "typing" ends.
closednoneThe connection went away; nothing is open any more.
typing_timeoutnoneThe reply took too long (CHAT_TYPING_TIMEOUT_MS, 12 seconds) to keep saying it is coming; "typing" ends.
openednoneThe connection opened; what is delivered by now is what a history may replace.
historyturnsAn earlier conversation, restored — see RestoredTurn.
clearednoneThe chat on screen goes: memory forgotten, sign-out, another person, or a new conversation.

foldChat

Use it to assemble a chat of your own from the client's events, exactly as the SDK's chat does.

function foldChat(state: ChatState, e: ChatEvent): ChatState
ParameterTypeDefaultMeaning
stateChatStaterequiredThe chat so far — start from EMPTY_CHAT.
eChatEventrequiredWhat just happened.

Returns — the next ChatState; the same object when nothing changed, so a React state update can be skipped. A new agent turn is proactive when no message of the customer's was waiting for a reply.

Errors — none.

const [chat, setChat] = useState(EMPTY_CHAT);
useEffect(() => {
  const names = ["chunk", "turn_start", "turn_end", "text_sent", "widget", "widget_pending", "close", "open", "history", "chat_cleared"];
  const offs = names.map((n) => client.on(n, (p) => { const e = chatEventFromClient(n, p); if (e) setChat((s) => foldChat(s, e)); }));
  return () => offs.forEach((off) => off());
}, [client]);

chatEventFromClient

Use it to turn a client event (chunk, turn_end, text_sent…) into the ChatEvent that foldChat takes.

function chatEventFromClient(event: string, payload?: unknown): ChatEvent | null
ParameterTypeDefaultMeaning
eventstringrequiredThe client event's name.
payloadunknownnoneWhat the event carried.

Returns — chunk → agent_text; turn_start → agent_turn_start; turn_end → agent_turn_end; text_sent → customer_sent; widget and widget_pending → widget; close → closed; open → opened; history → history; chat_cleared → cleared; anything else, or a payload that cannot be read, → null. The customer's own messages are yours to fold ({ type: "customer", … }) when you call sendText. Example value: { type: "agent_text", text: "Sure — " }.

Errors — none.

const e = chatEventFromClient("chunk", "Sure — ");

EMPTY_CHAT

Use it as the starting state of a chat you assemble with foldChat.

const EMPTY_CHAT: ChatState = { turns: [], typing: false, awaitingReply: false };

What to expect — A fixed value; foldChat returns it again on cleared.

CHAT_TYPING_TIMEOUT_MS

Use it to time out your typing indicator as the SDK does: after this long with nothing arriving, fold a typing_timeout.

const CHAT_TYPING_TIMEOUT_MS = 12_000;

What to expect — 12 seconds.

CHAT_HISTORY_CAP

Read it to know how many messages a chat keeps: older ones are dropped beyond it.

const CHAT_HISTORY_CAP = 200;

What to expect — 200 turns.

ChatRow

Read it to draw a chat window's rows in order: a bubble per message, one row per voice call, and day separators.

type ChatRow<T> =
  | { kind: "day"; key: string; day: string; label: string }
  | { kind: "voice"; key: string; at: number; first: number; last: number; label: string }
  | { kind: "turn"; key: string; index: number; item: T };

What to expect — A day separator comes before the first row when it is not from today, and wherever the device's day changes. A conversation that is all today has no separators. A voice call is one row however many turns it had — the window shows that a call happened, never its transcript.

kindFieldsMeaning
daykey, day, labelA day separator: day is "today", "yesterday" or the local date "YYYY-MM-DD"; label is "Today", "Yesterday" or the date in the device's words.
voicekey, at, first, last, labelOne row for a run of voice turns on one day: at is when the call's first turn was said, first and last are the indexes of its first and last turns, label reads "Voice call · 6:05 PM".
turnkey, index, itemOne message: item is your item, index its position in the list you passed.

Every row's key is stable — use it as the list key.

ChatRowOptions

Read it to fix the clock, the time zone or the wording chatRows uses — tests and previews, mostly.

FieldTypeDefaultMeaning
nownumbernowThe moment "Today" and "Yesterday" are measured from, in milliseconds since 1970.
utcOffsetMinutesnumberthe device's zoneA fixed offset east of UTC, in minutes, to read days and times in.
clock(at: number) => stringchatClockThe words for a time, as in "6:05 PM".
date(at: number, sameYear: boolean) => stringchatDateThe words for a day that is neither today nor yesterday.

chatRows

Use it to lay out a chat window of your own from any list of items — with the day separators and the voice-call rows the SDK's windows draw.

function chatRows<T>(items: readonly T[], read: (item: T) => { at?: number | null; voice?: boolean }, options: ChatRowOptions = {}): ChatRow<T>[]
ParameterTypeDefaultMeaning
itemsreadonly T[]requiredYour messages, oldest first.
read(item: T) => { at?: number | null; voice?: boolean }requiredSays when an item was said (milliseconds since 1970) and whether on a call. An item with no time — a component — joins the day before it.
optionsChatRowOptions{}The clock, the zone and the wording — see ChatRowOptions.

Returns — the rows to draw, in order; see ChatRow. Example value: [{ kind: "day", key: "day-2026-10-14", day: "yesterday", label: "Yesterday" }, { kind: "turn", key: "turn-0", index: 0, item: … }].

Errors — none.

const { messages } = useGraineAgent();
const rows = chatRows(messages, (m) => ({ at: m.at, voice: m.channel === "voice" }));

chatTurnRows

Use it to lay out the SDK's own turns (ChatTurn[]) without writing read yourself.

function chatTurnRows(turns: readonly ChatTurn[], options: ChatRowOptions = {}): ChatRow<ChatTurn>[]
ParameterTypeDefaultMeaning
turnsreadonly ChatTurn[]requiredThe chat's turns, oldest first.
optionsChatRowOptions{}As for chatRows.

Returns — the rows, as chatRows returns them.

Errors — none.

const [chat] = useState(EMPTY_CHAT);   // the chat you fold with foldChat
const rows = chatTurnRows(chat.turns);

chatClock

Use it to print a message's time the way the SDK's chat does.

function chatClock(at: number, utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
atnumberrequiredThe moment, in milliseconds since 1970.
utcOffsetMinutesnumberthe device's zoneA fixed offset east of UTC, in minutes.

Returns — the time in the device's own words and 12- or 24-hour setting. Example value: "6:05 PM".

Errors — none.

chatClock(Date.now());

chatDate

Use it to print a day that is neither today nor yesterday, the way the SDK's chat does.

function chatDate(at: number, sameYear: boolean, utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
atnumberrequiredThe moment, in milliseconds since 1970.
sameYearbooleanrequiredtrue to leave the year out (the day is in the current year).
utcOffsetMinutesnumberthe device's zoneA fixed offset east of UTC, in minutes.

Returns — the weekday, day and month in the device's own words — "Tue 14 Oct" on an English (UK) phone, "Tue, Oct 14" on an English (US) one — with the year when sameYear is false. Example value: "Tue 14 Oct".

Errors — none.

const at = Date.now() - 3 * 86_400_000;   // three days ago
chatDate(at, new Date(at).getFullYear() === new Date().getFullYear());

voiceCallLabel

Use it to word the row that stands for an earlier call in a chat window.

function voiceCallLabel(at: number, clock: (at: number) => string = chatClock): string
ParameterTypeDefaultMeaning
atnumberrequiredWhen the call's first turn was said.
clock(at: number) => stringchatClockThe words for that time.

Returns — "Voice call · " and the time. Example value: "Voice call · 6:05 PM".

Errors — none.

voiceCallLabel(new Date(2026, 9, 14, 18, 5).getTime()); // "Voice call · 6:05 PM"

chatIsNew

Use it to decide whether to show the brand's conversation starters: only on a new chat.

function chatIsNew(turns: readonly { role?: string; restored?: boolean }[]): boolean
ParameterTypeDefaultMeaning
turnsreadonly { role?: string; restored?: boolean }[]requiredThe turns on screen.

Returns — true while the customer has said nothing and no earlier conversation is on screen. Example value: true.

Errors — none.

const { messages } = useGraineAgent();
const showStarters = chatIsNew(messages);

chatPieces

Use it to decide what a chat window of your own draws: which turns, the starters, the "New conversation" control, the booked-calls card and — since 0.42.0 — "Book a call".

function chatPieces<T>(turns: readonly T[], chat: ChatPieceTokens | null | undefined, upcomingCount = 0, bookingAvailable = false): ChatPieces<T>
ParameterTypeDefaultMeaning
turnsreadonly T[]requiredEverything in the thread, oldest first; items with restored: true are earlier messages.
chatChatPieceTokens | null | undefinedrequiredThe design's chat settings — useGraineTheme().chat.
upcomingCountnumber0How many booked calls the customer has (upcomingCalls(…).length).
bookingAvailablebooleanfalse0.42.0. Whether booking is available here (client.bookingAvailable).

Returns — a ChatPieces. Example value: { turns: [ … ], starters: false, newConversation: true, upcoming: true, upcomingTitle: "Upcoming call", book: true }.

Errors — none.

const { messages } = useGraineAgent();
const theme = useGraineTheme();
const { calls } = useGraineScheduling();
const pieces = chatPieces(messages, theme.chat, upcomingCalls(calls).length, client.bookingAvailable);

ChatPieces

Read it to draw exactly the pieces the brand's design keeps.

What to expect — With earlier messages hidden by the design, the window opens like a new chat: the restored turns are not drawn (the agent still has them), the starters may show, and "New conversation" does not.

FieldTypeDefaultMeaning
turnsT[]requiredThe turns to draw, oldest first — without the restored ones while the design hides earlier messages.
startersbooleanrequiredWhether the brand's conversation starters may show: nothing restored on screen, nothing said by the customer.
newConversationbooleanrequiredWhether the header shows "New conversation": the design allows it and earlier messages are on screen.
upcomingbooleanrequiredWhether to draw the booked-calls card: the design keeps it and at least one call is booked.
upcomingTitlestringrequiredThe card's title: the brand's words, else "Upcoming call" or "Upcoming calls".
bookbooleanrequired0.42.0. Whether to offer "Book a call" — at the foot of the booked-calls card, or as a row of its own in the card's place when nothing is booked: the design keeps the booked-calls card and booking is available here.

ChatPieceTokens

Read it to see the design settings chatPieces reads: a value that is not a boolean (or, for the title, not a string) counts as the default.

FieldTypeDefaultMeaning
showUpcomingCallsunknownshownfalse hides the booked-calls card — and with it "Book a call".
upcomingTitleunknown"Upcoming call" / "Upcoming calls"The card's title, when the brand wrote one.
showHistoryunknownshownfalse keeps earlier messages off screen (the agent still has them).
newConversationunknownshownfalse hides the header's "New conversation" control (newConversation() still works).

DEFAULT_UPCOMING_TITLE

Use it to title a booked-calls card where the brand wrote no title.

const DEFAULT_UPCOMING_TITLE: Readonly<{ one: string; many: string }> = { one: "Upcoming call", many: "Upcoming calls" };

What to expect — one for a single booked call, many for more.

ChatSize

Read it to choose how big the chat window opens.

type ChatSize = "compact" | "large" | "full";

What to expect — Which size wins: the size passed to openChat(), then the app's chatSize, then the agent's Window size in the dashboard, else compact. An open's size lasts until the chat closes. How each size is drawn on a phone and on a tablet is chatPresentation.

ValueMeaning
"compact"A window above the voice bar.
"large"A sheet over most of a phone's screen; a panel on a tablet.
"full"The whole screen, inside the safe areas.

CHAT_SIZES

Use it to list the sizes in a settings screen of your own.

const CHAT_SIZES: ReadonlyArray<ChatSize> = ["compact", "large", "full"];

What to expect — A fixed list, smallest first.

chatSizeOf

Use it to settle which size a chat opens at, as the SDK does: the first real size among the candidates.

function chatSizeOf(...candidates: unknown[]): ChatSize
ParameterTypeDefaultMeaning
candidatesunknown[]noneThe sizes in order of precedence — the open's own, the app's chatSize, the design's.

Returns — the first candidate that is a size this release knows, else "compact". Example value: "large".

Errors — none.

chatSizeOf(undefined, "large", "full"); // "large"

The voice bar

Use these to put the agent's voice in your app: the engine that holds the call, the bar and buttons drawn in the agent's design, loaders and captions, the connection's quality, and — for an app with its own audio — the lower-level voice pieces.

GraineVoiceLauncher

Mount it once, near the root and inside the provider, to own the voice call: it loads Graine's voice engine in a hidden WebView, places and ends the call, and hands your UI the call's state through its render prop.

function GraineVoiceLauncher(props: GraineVoiceLauncherProps): JSX.Element

Props — GraineVoiceLauncherProps.

Returns — the hidden engine and whatever children(api) renders; api is a GraineVoiceApi.

Errors — throws outside a GraineProvider. Nothing else throws: a call that cannot be set up reaches onError and api.error; a refused microphone reaches onMicDenied and api.micDenied.

What to expect — Mounted with active={false} it loads the engine idle — nothing is registered or started — so the customer's tap later is one quick step. A page that fails to load is retried on the timing.retryDelaysMs ladder; after the last try api.connectivity.state is "failed" and onError says "The assistant could not load. Check your connection and try again." The call ends two seconds after the app leaves the foreground (unless the provider's endOnBackground is off). While a call from the calls package rings or is live, a start is refused with "A call is already in progress." One conversation at a time: starting the call closes an open chat.

<GraineVoiceLauncher webView={WebView} active={open} autoStart onEnded={() => setOpen(false)}>
  {(api) => (open ? <GraineVoiceBar api={api} onClose={() => setOpen(false)} /> : null)}
</GraineVoiceLauncher>

GraineVoiceLauncherProps

Read it to wire the launcher: the WebView, when the engine is active, how audio travels, and the callbacks your UI needs.

PropTypeDefaultMeaning
transport"ws" | "rtc"the agent's settingHow the call's audio travels. "ws": over the same connection as the screen context and actions. "rtc": WebRTC — packet-loss concealment, a jitter buffer and the phone's own echo cancellation — falling back to "ws" when WebRTC is unavailable (see onTransport). Leave it out and the agent's Embed & Widgets setting decides, so it can change without a release.
onTransport(info: { transport: "ws" | "rtc"; requested: "ws" | "rtc"; reason?: "not_configured" | "unreachable" | "screen_channel"; message?: string }) => voidnoneHear how the call actually travelled, including after a fallback: reason "not_configured" is a setting to change in the dashboard (it never succeeds on retry), "unreachable" is the network blocking WebRTC, "screen_channel" means the audio connected but the screen context could not, so the call restarted the other way rather than run blind; message is Graine's sentence naming the setting.
webViewReact.ComponentType<any>requiredWebView from react-native-webview, passed in so the SDK does not depend on it.
autoStartbooleanfalsePlace the call as soon as the engine is ready and active is true — for a bar that opens straight into a call.
activebooleantruefalse loads the engine idle before the customer needs it; true makes it ready (with autoStart, it places the call; without, it warms up so the tap is instant). Turning it back to false ends a live call, as unmounting does.
onCaption(c: { role: string; text: string; live: boolean }) => voidnoneHear each caption line as it is said — role "agent" or "customer", live while it is still being said. api.captions keeps the transcript for you.
onCallState(s: { connected: boolean; connecting: boolean }) => voidnoneHear the call start connecting, connect, or end — connected and connecting as they now stand.
onEnded() => voidnoneHear the call end — once, on the way out of a live call — to close your UI. Give the goodbye a moment to finish before you unmount.
onMicDenied(reason: string) => voidnoneHear that the microphone was refused ("NotAllowedError") or that the device has none usable ("NotFoundError") — show how to allow it.
onError(message: string) => voidnoneHear that voice cannot work until something changes: the key was rejected, the agent is not open to apps, the engine could not load after every retry, or a call from the calls package holds the line ("A call is already in progress."). The same sentence reaches useGraineEvents as an error event.
timing{ readyTimeoutMs?: number; retryDelaysMs?: number[]; stallMs?: number; bootMs?: number; hardCapMs?: number; endWatchdogMs?: number; slowConnectMs?: number }see belowTune how long the engine may take to load and how often it retries: readyTimeoutMs 15000 (load to ready); retryDelaysMs [1000, 2000, 4000, 8000, 15000, 15000] (its length is the number of reloads before "failed"); stallMs (nothing arriving) and bootMs (loaded but not ready), both readyTimeoutMs; hardCapMs 120000 (one attempt, however steadily it loads); endWatchdogMs 4000 (how long a hang-up may go unconfirmed before the call is ended on the device); slowConnectMs 4000 (when api.slowConnect turns on).
children(api: GraineVoiceApi) => React.ReactNodenoneDraw your UI from the call — usually <GraineVoiceBar api={api} />.

GraineVoiceApi

Read it in GraineVoiceLauncher's render prop to draw a call UI: where the call is, the controls, the captions and the connection.

What to expect — Re-rendered as the call changes. captions is cleared when a call starts and when the launcher becomes inactive.

FieldTypeDefaultMeaning
phase"idle" | "connecting" | "reconnecting" | "thinking" | "speaking" | "listening"requiredWhere the call is, for a bar that shows one thing at a time: idle (no call, ready), connecting (loading or placing), reconnecting (the engine is reloading), thinking (the customer finished; the agent has not answered), speaking (the agent is talking), listening (the agent waits for the customer). Pair it with <GraineLoader moment="thinking" />.
connectivity{ state: "loading" | "ready" | "retrying" | "failed"; attempt: number; reason?: string }requiredWhether the voice engine is there: loading (first load), ready, retrying (a load failed; a reload is scheduled), failed (every retry used — say so and offer retry()); attempt counts reloads (0 on the first load); reason says why the last attempt was abandoned.
retry() => voidrequiredReload the engine and start the retry ladder again — for a "Try again" button.
connectedbooleanrequiredWhether the call is connected — the agent can hear the customer.
connectingbooleanrequiredWhether a call is being placed.
mutedbooleanrequiredWhether the customer's microphone is muted on this call — draw your mute button from it.
readybooleanrequiredWhether a call can start now; false while the agent's settings are still loading, when a tap would do nothing — draw a waiting state instead.
start() => voidrequiredPlace the call. Pressed before the engine is ready, it waits and places it the moment it is.
end() => voidrequiredHang up.
setMuted(m: boolean) => voidrequiredMute or unmute the customer on this call.
captionsCaption[]requiredWhat has been said so far, oldest first — both sides, a live line replaced in place as words arrive; at most CAPTION_HISTORY lines.
captionCaption | nullrequiredThe line being said now, or the last one said; null before any.
captionsOnbooleanrequiredThe captions switch — true by default — so every screen draws captions the same way.
setCaptionsOn(on: boolean) => voidrequiredTurn captions on or off.
clearCaptions() => voidrequiredClear the transcript and keep the call going.
qualityConnectionQualityrequiredThe call's connection in one word — excellent, good, poor, lost, or unknown outside a call; show "weak connection" from it.
network"online" | "offline" | "unknown"requiredWhether the device is online, as the engine sees it.
reconnectingbooleanrequiredA call interrupted by the network is being re-established; the transcript is kept and phase reads reconnecting.
errorstring | nullrequiredWhy voice cannot work until a setting is fixed (a key, an agent not set up for apps), else null. Also sent to onError.
micDeniedbooleanrequiredtrue if the customer turned down microphone access at the most recent start. Also sent to onMicDenied.
connectStage"registering" | "dialling" | "retrying" | "switching" | nullrequiredWhere a call being placed is: registering (setting up, on a cold start), dialling (on its way to the agent), retrying (the first attempt went unanswered; trying a fresh connection — on mobile data, usually one the network dropped while idle), switching (moving to the other way of carrying audio); null outside a connect. Say "Trying again…" across a retry.
slowConnectbooleanrequiredThe connect has run past timing.slowConnectMs (4 seconds) — say "Still connecting…".
callFailedbooleanrequiredThe last start could not connect at all. Unlike error, nothing needs fixing: start() simply tries again.

CAPTION_HISTORY

Read it to know how many lines api.captions keeps.

const CAPTION_HISTORY = 12;

What to expect — 12 lines, oldest dropped first. A caption box usually shows the last two or three.

Caption

Read it to draw one caption line.

FieldTypeDefaultMeaning
role"agent" | "customer"requiredWho is speaking — style the line by it.
textstringrequiredWhat has been said so far in this line.
livebooleanrequiredtrue while the line is still being said or recognised — draw it as provisional.

GraineVoiceBar

Use it as the call's UI: the bar, the chat window (where the agent offers chat), the components the agent draws and the booked-calls card, drawn in the design chosen in the dashboard.

function GraineVoiceBar(props: GraineVoiceBarProps): JSX.Element

Props — GraineVoiceBarProps.

Returns — the bar, positioned over your app at the bottom.

Errors — throws outside a GraineProvider.

What to expect — It follows the conversation by itself: the call's controls, captions, the switch to the chat, a chat window at the size the design (or openChat) asks for, the components the agent draws (unless your app mounts its own GraineWidget), and — since 0.42.0 — the customer's booked calls with Cancel and Book a call where booking is available. It rises above the keyboard and the customer can drag it.

{(api) => <GraineVoiceBar api={api} onClose={() => setOpen(false)} bottomInset={tabBarHeight + 12} safeArea={insets} />}

GraineVoiceBarProps

Read it to place the bar and connect it to the call.

PropTypeDefaultMeaning
apiGraineVoiceApirequiredThe call — pass what GraineVoiceLauncher's render prop hands you.
onClose() => voidnoneHear the bar close — after a hang-up or the close button, once the goodbye has finished playing — to unmount it.
bottomInsetnumberthe provider's inset for this screen, else 96How far above the bottom edge the bar sits, clear of your tab bar. A value you pass wins.
draggablebooleantrueLet the customer drag the bar up and down.
placement{ bottomOffset?: number } | nullnoneA position you saved, to restore: pixels above the bar's resting place.
onPlacementChange(placement: { bottomOffset: number }) => voidnoneHear where the customer let go — save bottomOffset to restore it next time.
namestringthe dashboard's headingWhat spoken labels call the agent — "Start talking to Acme". The name drawn on the bar is always the dashboard's heading.
accessoryReact.ReactNodenoneSomething to ride on the bar, just above it — a GraineBarAction that moves the conversation to a phone call, say. It moves with the bar and nothing of the bar's styling reaches it.
safeArea{ top?: number; bottom?: number }a typical phone'sThe phone's safe areas, for a chat that opens as a sheet or the whole screen — pass useSafeAreaInsets().

GraineBarAction

Use it for a button that rides on the bar (its accessory) and must look like part of it — in the design's accent, corners, type, shadow and icons.

function GraineBarAction(props: GraineBarActionProps): JSX.Element

Props — GraineBarActionProps.

Returns — the button.

Errors — none.

{(api) => <GraineVoiceBar api={api} accessory={<GraineBarAction label="Call Acme" icon="call" onPress={() => GraineCalls.startCall()} />} />}

GraineBarActionProps

Read it to say what the button says and does — its look is the design's.

PropTypeDefaultMeaning
labelstringrequiredThe button's text, which screen readers also read aloud.
icon"call" | "chat""call"The SDK icon before the label, in the design's icon style.
onPress() => voidrequiredWhat a tap does.

GraineLauncherButton

Use it as your launcher when you show and hide GraineVoiceBar yourself: the design's launcher, drawn as the floating button draws it.

function GraineLauncherButton(props: GraineLauncherButtonProps): JSX.Element

Props — GraineLauncherButtonProps.

Returns — the button; it shows the brand's logo or glyph in the design's launcher shape.

Errors — throws outside a GraineProvider, as useGraineAgent does.

{!open && <GraineLauncherButton onPress={() => setOpen(true)} style={{ position: "absolute", right: 16, bottom: 24 }} />}

GraineLauncherButtonProps

Read it to place the launcher and say what a tap does — its look is the design's.

PropTypeDefaultMeaning
onPress() => voidrequiredWhat a tap does — usually opening your bar.
styleViewStylenoneIts position on screen — for example { position: "absolute", right: 16, bottom: 24 }.
labelstring"Talk to " and the dashboard's headingWhat a screen reader says.
sizenumber56Its diameter, in points.

GraineFab

Use it when you want the whole flow done for you: a floating button that opens the conversation, folds back into a button when the call ends, and follows the launcher visibility rules. The provider mounts it for you with fab={{ webView: WebView }}.

function GraineFab(props: GraineFabProps): JSX.Element | null

Props — GraineFabProps.

Returns — the button (hidden where the visibility rules say, or when the agent cannot start), then the bar while a conversation is open; null when hidden.

Errors — throws outside a GraineProvider.

<GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_…" fab={{ webView: WebView, position: "bottom-left" }}>
  <App />
</GraineProvider>

GraineFabProps

Read it to tune the floating button: the corner, the size, and when the voice engine loads.

PropTypeDefaultMeaning
webViewComponentType<any>requiredWebView from react-native-webview: the voice engine runs in it.
position"bottom-right" | "bottom-left""bottom-right"Which bottom corner; the visibility rules' insets still apply.
sizenumber56The button's diameter, in points.
accentstringthe dashboard's accentA colour for this button only.
labelstring"Talk to " and the dashboard's headingWhat a screen reader says.
overlayComponentType<{ children?: React.ReactNode }>noneA wrapper to draw the button in, above native screens and modals — pass react-native-screens' FullWindowOverlay on iOS.
onOpen() => voidnoneHear the customer open the conversation.
onClose() => voidnoneHear the conversation end and the button come back.
prewarmbooleantrueLoad the voice engine before the customer taps, so the call connects in well under a second; false loads it on the tap and costs the customer a few seconds of "Connecting…".

GraineAgentBar

Use it for a ready-made bar over your app — status, transcript, captions and a composer — styled from the dashboard's Appearance (accent, heading, logo) with props to override; the floating button opens it. For the brand's full design from the Garage, use GraineVoiceBar. Mount it at your root, not inside a screen, so a navigation never unmounts the conversation.

function GraineAgentBar(props: GraineAgentBarProps): JSX.Element | null

Props — GraineAgentBarProps.

Returns — the bar, or null while hidden, while the visibility rules hide the launcher, or when the agent could not start (useGraineAgent().error).

Errors — throws outside a GraineProvider.

<GraineAgentBar name="Acme assistant" bottomInset={tabBarHeight + 12} />

GraineAgentBarProps

Read it to style and place the classic bar; the dashboard's appearance is the default and each prop overrides it.

PropTypeDefaultMeaning
accentstringthe dashboard's accentThe bar's accent colour.
namestringthe dashboard's headingThe agent's name beside the status — the name your customers hear.
avatarUrlstringthe dashboard's logoA picture of the agent.
bottomInsetnumber96How far above the bottom edge it sits, clear of your tab bar and primary buttons.
hiddenbooleanfalseHide it without unmounting the provider — on sign-in or payment screens, say.
captionsOnbooleanfalseStart with captions on; the customer can switch them from the bar.
scheme"dark" | "light" | "auto""auto"Which palette to draw; "auto" follows the device.
themePartial<GraineBarTheme>the scheme's paletteOverride single colours of the palette — see GraineBarTheme.
draggablebooleantrueLet the customer drag the bar up and down; a tap still works.
placement{ bottomOffset?: number } | nullnoneA position you saved, to restore: pixels above the resting place.
onPlacementChange(placement: { bottomOffset: number }) => voidnoneHear where the customer let go — save bottomOffset.

GraineBarTheme

Read it to override the classic bar's colours one at a time; the accent comes from the dashboard.

FieldTypeDefaultMeaning
surfacestringthe scheme'sThe bar's and panel's background.
borderstringthe scheme'sThe hairline around them.
textstringthe scheme'sMain text: the agent's name, replies, what the customer types.
substringthe scheme'sText with less emphasis — the status line, placeholders and the empty state.
chipstringthe scheme'sUsed for the agent's message bubbles and for the understated round buttons.
onAccentstringthe scheme'sText and icons drawn on the accent, and on danger.
dangerstringthe scheme'sThe muted state — the one state with a colour of its own.

GraineLauncher

Keep it only in an integration that already uses it: it is GraineAgentBar under its earlier name, deprecated.

const GraineLauncher: typeof GraineAgentBar;

Props — GraineLauncherProps, the same as GraineAgentBarProps.

Returns, Errors — as GraineAgentBar.

<GraineLauncher name="Acme assistant" />   // prefer <GraineAgentBar … />

GraineLauncherProps

Read it as GraineAgentBarProps — the same props under the deprecated component's name.

type GraineLauncherProps = GraineAgentBarProps;

What to expect — Every prop of GraineAgentBarProps, with the same defaults.

GraineLoader

Use it wherever the customer waits — connecting, reconnecting, the agent thinking, a component loading — so waiting looks as the dashboard's Appearance says (kind, size, speed, colour and words).

function GraineLoader(props: GraineLoaderProps): JSX.Element

Props — GraineLoaderProps.

Returns — the loader: the mark, and the words for the moment beside or under it. It holds still when the phone asks for reduced motion, and reads as a progress bar to screen readers.

Errors — none: outside a provider it draws the defaults.

{(api) => (
  <View>
    {api.phase === "thinking" && <GraineLoader moment="thinking" />}
    <GraineVoiceBar api={api} />
  </View>
)}

GraineLoaderProps

Read it to choose the moment the loader is for; everything else defaults to the dashboard's choice.

PropTypeDefaultMeaning
momentLoaderMomentnoneWhich of the dashboard's words to show: "connecting", "reconnecting", "thinking" or "component". Leave it out for no words.
labelstringthe moment's wordsWords of your own, over the moment's.
kindLoaderKindthe dashboard's ("dots")The animation: "dots", "pulse", "ring", "bars" or "mark" (the brand's logo, breathing).
colorstringthe dashboard's, else the accentThe mark's colour.
sizenumberthe dashboard's size (20)The mark's size, in points.
stackedbooleanfalsePut the words under the mark instead of beside it.

useGraineLoader

Call it when you draw your own loader but want the dashboard's choice: the kind, size, speed, colour and words.

function useGraineLoader(): LoaderLook & { accent: string }

Parameters — none.

Returns — the dashboard's loader resolved into numbers — see LoaderLook — plus accent, the colour to draw it in (the loader's own colour, else the design's accent). Example value: { kind: "dots", color: null, px: 20, periodMs: 1100, text: { connecting: "Connecting…", … }, skeleton: false, accent: "#3740B8" }.

Errors — none: outside a provider it returns the defaults.

const look = useGraineLoader();
<MySpinner size={look.px} color={look.accent} durationMs={look.periodMs} />

GraineInAppClient.setMuted

Call it to mute or unmute the customer on this client's own voice path (useGraineVoice); the voice launcher's call has its own api.setMuted.

setMuted(muted: boolean): void
ParameterTypeDefaultMeaning
mutedbooleanrequiredtrue to stop sending the customer's voice; false to send it again.

Returns — nothing. The microphone keeps running and nothing leaves the device while muted, so unmuting is instant. A change is announced as the muted event.

Errors — none.

client.setMuted(!client.isMuted);

GraineInAppClient.isMuted

Read it to draw the mute button of a voice UI built on this client.

get isMuted(): boolean

Returns — true while this client is holding the customer's voice back. Example value: false.

Errors — none.

const label = client.isMuted ? "Unmute" : "Mute";

GraineInAppClient.isAgentSpeaking

Read it to know whether the agent is mid-reply on this client's connection.

get isAgentSpeaking(): boolean

Returns — on a typed chat, true from the first word of a reply until it ends; on a voice connection, true while the agent's voice is still playing. Example value: true.

Errors — none.

if (client.isAgentSpeaking) showSpeakingDot();

useGraineConnection

Call it in a voice UI to say "Weak connection" or "Offline" when the network is the problem, so the agent does not take the blame.

function useGraineConnection(): { quality: ConnectionQuality; online: boolean | null; network: "online" | "offline" | "unknown" }

Parameters — none.

Returns — quality: the call's connection in one word (see ConnectionQuality); online: true or false once the SDK knows, else null; network: the same as a word. Example value: { quality: "poor", online: true, network: "online" }.

Errors — throws outside a GraineProvider.

const { quality, online } = useGraineConnection();
{online === false ? <Banner text="You're offline" /> : quality === "poor" ? <Banner text="Weak connection" /> : null}

GraineInAppClient.connectionQuality

Read it outside React to know how good the call's connection is.

get connectionQuality(): ConnectionQuality

Returns — the last measured quality; "unknown" outside a call, "lost" while the device is offline. Example value: "good".

Errors — none.

console.log("quality", client.connectionQuality);

GraineInAppClient.networkState

Read it outside React to know whether the device is online, as the voice engine last reported.

get networkState(): "online" | "offline" | "unknown"

Returns — "unknown" until the engine reports. Example value: "online".

Errors — none.

if (client.networkState === "offline") showOfflineBanner();

GraineInAppClient.reportQuality

The voice engine calls it with each connection measurement; call it yourself only when you carry the call's audio and can measure it.

reportQuality(sample: QualitySample | null): ConnectionQuality
ParameterTypeDefaultMeaning
sampleQualitySample | nullrequiredRound-trip time, jitter and loss as measured; null when there is no call.

Returns — the quality the sample amounts to ("unknown" for null). The quality event fires only when the word changes. Example value: "good".

Errors — none.

client.reportQuality({ rtt: 180, jitter: 12, loss: 0.01 }); // "good"

GraineInAppClient.reportNetwork

The voice engine calls it when the device goes offline or comes back; call it yourself only when you carry the call.

reportNetwork(online: boolean): void
ParameterTypeDefaultMeaning
onlinebooleanrequiredtrue when the device is online again, false when it went offline.

Returns — nothing. Going offline also sets the quality to "lost". A change is announced as the network event.

Errors — none.

import NetInfo from "@react-native-community/netinfo";
 
NetInfo.addEventListener((s) => client.reportNetwork(!!s.isConnected));

ConnectionQuality

Read it to show the connection as one word.

type ConnectionQuality = "excellent" | "good" | "poor" | "lost" | "unknown";

What to expect — Measured every couple of seconds while a call is up and changed only when the word changes. The worst of the three measurements decides.

ValueMeaning
"excellent"Round trip under 150 ms, jitter under 30 ms, loss under 2%.
"good"Any of: round trip from 150 ms, jitter from 30 ms, or loss from 2% — still a clear conversation.
"poor"Any of: round trip from 400 ms, jitter from 80 ms, or loss from 8% — say "Weak connection".
"lost"The device went offline.
"unknown"No call, or nothing measured yet.

QualitySample

Read it to describe one measurement of a call's connection, for reportQuality and classifyQuality.

FieldTypeDefaultMeaning
rttnumber | nulloptionalRound-trip time to the media server, in milliseconds.
jitternumber | nulloptionalHow much arrival times vary, in milliseconds.
lossnumber | nulloptionalThe share of packets lost, from 0 to 1.

classifyQuality

Use it to turn a measurement into the word the SDK would show, with the same thresholds as the app and the website.

function classifyQuality(s: QualitySample | null | undefined): ConnectionQuality
ParameterTypeDefaultMeaning
sQualitySample | null | undefinedrequiredThe measurement.

Returns — "excellent", "good" or "poor" by the thresholds in ConnectionQuality; "unknown" when nothing was measured. It never returns "lost". Example value: "poor".

Errors — none.

classifyQuality({ rtt: 450 }); // "poor"

GraineInAppClient.hasVoiceHost

Read it to know whether a GraineVoiceLauncher is mounted for this client — the provider then leaves the call to it.

get hasVoiceHost(): boolean

Returns — true while a launcher is mounted. Example value: true.

Errors — none.

if (!client.hasVoiceHost) console.warn("No voice launcher is mounted");

GraineInAppClient.setVoiceHost

GraineVoiceLauncher calls it as it mounts and unmounts; your app does not need it.

setVoiceHost(on: boolean): void
ParameterTypeDefaultMeaning
onbooleanrequiredtrue as a launcher mounts, false as it unmounts.

Returns — nothing. A change is announced as the voice_host event ({ present }).

Errors — none.

client.setVoiceHost(true);

GraineInAppClient.setHostCallLive

GraineVoiceLauncher calls it as its call connects and ends; your app does not need it.

setHostCallLive(live: boolean): void
ParameterTypeDefaultMeaning
livebooleanrequiredtrue while the launcher's call is connected; false when it ends.

Returns — nothing. Messages typed before the call connected are handed to it now (each announced as text_sent).

Errors — none.

client.setHostCallLive(true);

useGraineVoice

Call it only when your app brings its own audio module instead of GraineVoiceLauncher: it runs a spoken conversation over the client's connection with your adapter. Pass voice to the provider as well, or the agent answers in text.

function useGraineVoice(adapter: AudioAdapter | null, options: VoiceSessionOptions = {}): { listening: boolean; agentSpeaking: boolean; start: () => Promise<void>; stop: () => Promise<void> }
ParameterTypeDefaultMeaning
adapterAudioAdapter | nullrequiredYour microphone and speaker — see AudioAdapter; null until it is ready.
optionsVoiceSessionOptions{}Callbacks for the customer's recognised speech and the agent speaking — see VoiceSessionOptions.

Returns — listening: the microphone is being captured; agentSpeaking: the agent's voice is playing; start(): begin capturing and playing; stop(): stop both. Example value: { listening: false, agentSpeaking: false, start: [Function], stop: [Function] }.

Errors — throws outside a GraineProvider. start() rejects with Error("[Graine] useGraineVoice needs an audio adapter.") while adapter is null, and with whatever your adapter's startCapture throws. Microphone permission must be granted before start() — the hook does not ask.

const { listening, start, stop } = useGraineVoice(adapter, { onTranscript: (text, final) => final && setHeard(text) });

VoiceSession

Use it outside React to wire your audio adapter to a client's spoken conversation — useGraineVoice wraps it.

class VoiceSession {
  constructor(client: GraineInAppClient, adapter: AudioAdapter, options: VoiceSessionOptions = {})
  get active(): boolean
  async start(): Promise<void>
  async stop(): Promise<void>
}
ParameterTypeDefaultMeaning
clientGraineInAppClientrequiredThe client whose connection carries the conversation (built with voice: true).
adapterAudioAdapterrequiredYour microphone and speaker.
optionsVoiceSessionOptions{}Callbacks — see VoiceSessionOptions.
MemberTypeMeaning
constructor(client, adapter, options?)Prepares a session; nothing starts until start().
activebooleantrue between start() and stop() — the microphone is being captured.
start() => Promise<void>Plays the agent's audio through your adapter (working out its format once per call) and sends the customer's, holding back the agent's own echo; tells the client how much audio is still queued so turn-taking is timed at playout. Rejects with what your adapter's startCapture throws. Calling it again while active does nothing.
stop() => Promise<void>Stops capture, drops queued playback and listens no more. Calling it while stopped does nothing.

Errors — start() rejects with your adapter's errors; nothing else throws.

const session = new VoiceSession(client, adapter, { onAgentSpeaking: setSpeaking });
await client.connect();
await session.start();

VoiceSessionOptions

Read it to hear what the customer says and when the agent speaks, during a session you run.

FieldTypeDefaultMeaning
onTranscript(text: string, final: boolean) => voidnoneHear the customer's speech as it is recognised — final true for the settled sentence.
onAgentSpeaking(speaking: boolean) => voidnoneHear the agent start and stop speaking — drive your own speaking indicator.

AudioAdapter

Implement it to connect your audio module to useGraineVoice or VoiceSession: capture the microphone, play the agent.

interface AudioAdapter {
  startCapture(onChunk: (base64: string) => void): Promise<void> | void;
  stopCapture(): Promise<void> | void;
  play(pcm16: Int16Array, sampleRate: number): void;
  clear(): void;
  bufferedSeconds?(): number;
}

What to expect — Capture must be 16 kHz mono 16-bit audio (CAPTURE_SAMPLE_RATE), base64-encoded, with permission already granted; a different rate is not refused — it produces a confident, wrong transcript.

MemberTypeDefaultMeaning
startCapture(onChunk: (base64: string) => void) => Promise<void> | voidrequiredStart the microphone and call onChunk with each chunk.
stopCapture() => Promise<void> | voidrequiredStop the microphone.
play(pcm16: Int16Array, sampleRate: number) => voidrequiredQueue a chunk of the agent's voice for playback. Queue it with a small cushion rather than playing at once, or the voice breaks up.
clear() => voidrequiredDrop everything queued and unplayed — the customer interrupted.
bufferedSeconds() => numberoptionalSeconds of the agent's voice still waiting to be heard. With it, turn-taking is timed to what the customer actually hears; without it, the timing is optimistic.

liveAudioStreamAdapter

Use it to build an AudioAdapter from react-native-live-audio-stream for capture and any player you pass for playback.

function liveAudioStreamAdapter(LiveAudioStream: any, sink: Pick<AudioAdapter, "play" | "clear">): AudioAdapter
ParameterTypeDefaultMeaning
LiveAudioStreamanyrequiredThe react-native-live-audio-stream module, passed in.
sinkPick<AudioAdapter, "play" | "clear">requiredYour player: queue the agent's audio, and drop it on an interruption.

Returns — an AudioAdapter that captures 16 kHz mono in chunks of about 40 ms (with Android's voice-recognition source, which turns on echo cancellation and noise suppression) and plays through sink.

Errors — none.

import LiveAudioStream from "react-native-live-audio-stream";
import { liveAudioStreamAdapter } from "@graineai/inapp-react-native";
 
const adapter = liveAudioStreamAdapter(LiveAudioStream, myPcmPlayer);   // myPcmPlayer: your player's play() and clear()

CAPTURE_SAMPLE_RATE

Read it to capture the microphone at the rate the agent expects.

const CAPTURE_SAMPLE_RATE = 16_000;

What to expect — 16 kHz mono; anything else must be resampled before it reaches the SDK.

AGENT_PCM_RATE

Read it to know the rate of the agent's voice on a spoken connection when it arrives as raw samples with no format declared.

const AGENT_PCM_RATE = 24_000;

What to expect — 24 kHz. Played at any other rate the agent sounds too slow and too low (or too fast).

GraineInAppClient.sendAudio

VoiceSession calls it with each chunk of the customer's voice; call it yourself only when you carry the audio without VoiceSession.

sendAudio(base64: string, sampleRate = 16_000): boolean
ParameterTypeDefaultMeaning
base64stringrequiredOne chunk of 16-bit mono audio, base64-encoded.
sampleRatenumber16000The chunk's sample rate, in hertz — keep it 16000.

Returns — true when the chunk was sent; false while muted (the chunk is dropped on the device) or with no open connection. The first chunk while the agent is speaking tells the agent the customer is talking over it. Example value: true.

Errors — none.

adapter.startCapture((chunk) => client.sendAudio(chunk));

GraineInAppClient.endUtterance

Call it when your audio module detects that the customer stopped talking, so the agent answers promptly.

endUtterance(): boolean

Parameters — none.

Returns — true when the signal was sent; false with no open connection. Example value: true.

Errors — none.

vad.onSilence(() => client.endUtterance());

GraineInAppClient.setPlayoutClock

VoiceSession calls it so the client knows how much of the agent's voice is still waiting to be heard; call it yourself only when you play the agent's audio without VoiceSession.

setPlayoutClock(fn: (() => number) | null): void
ParameterTypeDefaultMeaning
fn(() => number) | nullrequiredReturns the seconds of the agent's voice still queued for playback; null to stop.

Returns — nothing. With it, the client tells the agent each piece was heard when it actually played (capped at 10 seconds); without it, at once.

Errors — none.

client.setPlayoutClock(() => player.queuedSeconds());

decodeAgentAudio

Use it to turn a frame of the agent's voice into playable samples when you play the agent's audio yourself — it works out whether the frame is µ-law or 16-bit PCM.

function decodeAgentAudio(bytes: Uint8Array, declared: { format?: string | null; sampleRate?: number | null } | null, latched: AudioFormat | null, fallbackPcmRate = 16_000): DecodedAudio | null
ParameterTypeDefaultMeaning
bytesUint8ArrayrequiredThe frame as it arrived (decode base64 with base64ToBytes).
declared{ format?: string | null; sampleRate?: number | null } | nullrequiredWhat Graine said about the frame, if anything — the audio event's format and sampleRate. A declared format is believed.
latchedAudioFormat | nullrequiredWhat this call's audio was already found to be (the last result's latch), or null at the start of a call.
fallbackPcmRatenumber16000The rate to assume for PCM nobody declared — pass AGENT_PCM_RATE on a spoken connection.

Returns — a DecodedAudio, or null for an empty frame. Keep its latch and pass it back as latched: once a frame loud enough to tell has settled the format, the rest of the call uses it. Example value: { pcm16: Int16Array(4800), sampleRate: 24000, kind: "pcm", latch: { kind: "pcm", sampleRate: 24000 } }.

Errors — none.

let latch: AudioFormat | null = null;
client.on("audio", (e) => {
  const out = decodeAgentAudio(base64ToBytes(e.data), { format: e.format, sampleRate: e.sampleRate }, latch, AGENT_PCM_RATE);
  if (out) { latch = out.latch ?? latch; player.play(out.pcm16, out.sampleRate); }
});

AudioFormat

Read it as what a call's audio turned out to be; keep it and pass it back to decodeAgentAudio.

FieldTypeDefaultMeaning
kind"mulaw" | "pcm"requiredµ-law (8-bit telephone audio) or 16-bit PCM.
sampleRatenumberrequiredSamples per second, in hertz.

DecodedAudio

Read it to play one decoded frame of the agent's voice.

FieldTypeDefaultMeaning
pcm16Int16ArrayrequiredThe samples, ready for a 16-bit player.
sampleRatenumberrequiredThe rate to play them at, in hertz.
kind"mulaw" | "pcm"requiredWhat the frame was read as.
latchAudioFormat | nullrequiredThe format, once a frame settled it — keep it and pass it back as latched; null while still undecided.

decodeMuLaw

Use it to turn µ-law bytes into 16-bit samples (ITU-T G.711).

function decodeMuLaw(bytes: Uint8Array): Int16Array
ParameterTypeDefaultMeaning
bytesUint8Arrayrequiredµ-law bytes, one per sample.

Returns — one 16-bit sample per byte.

Errors — none.

client.on("audio", (e) => {
  if (e.format === "mulaw") player.play(decodeMuLaw(base64ToBytes(e.data)), e.sampleRate ?? 8000);
});

decodePcm16

Use it to read little-endian 16-bit PCM bytes as samples.

function decodePcm16(bytes: Uint8Array): Int16Array
ParameterTypeDefaultMeaning
bytesUint8ArrayrequiredTwo bytes per sample, low byte first.

Returns — the samples; an odd trailing byte (half a sample) is dropped rather than played as a click.

Errors — none.

client.on("audio", (e) => {
  if (e.format === "pcm") player.play(decodePcm16(base64ToBytes(e.data)), e.sampleRate ?? AGENT_PCM_RATE);
});

base64ToBytes

Use it to decode base64 on any React Native engine, without depending on atob or Buffer.

function base64ToBytes(b64: string): Uint8Array
ParameterTypeDefaultMeaning
b64stringrequiredBase64 text.

Returns — the bytes.

Errors — none.

client.on("audio", (e) => {
  const bytes = base64ToBytes(e.data);   // the frame's raw bytes, for decodeAgentAudio
});

base64ToPcm16

Use it when you already know a base64 chunk is 16-bit PCM — for the agent's audio, prefer decodeAgentAudio, which does not assume.

function base64ToPcm16(b64: string): Int16Array
ParameterTypeDefaultMeaning
b64stringrequiredBase64-encoded little-endian 16-bit PCM.

Returns — the samples.

Errors — none.

adapter.startCapture((chunk) => {
  const pcm = base64ToPcm16(chunk);   // the microphone's 16-bit samples
});

EchoGuard

Use it when you send the microphone yourself on a phone's loudspeaker: it holds back the agent's own voice coming back through the microphone, and lets the customer through when they really talk over it. VoiceSession uses one for you.

class EchoGuard {
  constructor(opts?: { overEcho?: number; overNoise?: number; minLevel?: number; sustainMs?: number; preRollFrames?: number; warmupFrames?: number })
  get threshold(): number
  get interrupting(): boolean
  reset(): void
  filter(base64: string, pcm: Int16Array, agentAudible: boolean, frameMs: number): string[]
}
MemberTypeMeaning
constructor(opts?)Tune it — every option is optional: overEcho (2.2) how far above the measured echo a level must be, overNoise (4) how far above the room's noise, minLevel (0.012) an absolute floor, sustainMs (240) how long it must last to count as the customer, preRollFrames (12) how much held audio to send ahead of a confirmed interruption, warmupFrames (10) how many frames at the start of each agent utterance are spent measuring its echo.
thresholdnumberHow loud a frame has to be, at this moment, to be taken as the customer speaking.
interruptingbooleantrue once the customer has been loud enough for long enough to be talking over the agent.
reset() => voidForget what the echo sounded like — at the start of each call.
filter(base64, pcm, agentAudible, frameMs) => string[]Decide what to send for one captured frame: base64 the frame as captured, pcm the same frame decoded, agentAudible whether the agent's voice is playing now, frameMs the frame's length. Returns the frames to send, oldest first — empty while it is only echo.

Errors — none.

const guard = new EchoGuard();
adapter.startCapture((chunk) => {
  const frames = guard.filter(chunk, base64ToPcm16(chunk), client.isAgentSpeaking, 40);
  frames.forEach((f) => client.sendAudio(f));
});

RtcVoiceSessionController

Use it for WebRTC voice of your own, outside GraineVoiceLauncher: it places a WebRTC call to the agent with your publishable key. It needs react-native-webrtc and the optional WebRTC peer dependency the package declares — a native module, so a store rebuild. Screen context and actions still travel on the client's connection.

class RtcVoiceSessionController {
  constructor(opts: RtcVoiceOptions)
  readonly sessionId: string
  getState(): RtcVoiceState
  async start(): Promise<void>
  setMuted(muted: boolean): void
  async stop(): Promise<void>
}
MemberTypeMeaning
constructor(opts: RtcVoiceOptions)Prepares a controller; nothing connects until start(). See RtcVoiceOptions.
sessionIdstringThis call's id (yours from opts.sessionId, else a fresh one) — the same id ties the call to the client's connection, so the agent sees the screen during it.
getState() => RtcVoiceStateWhere the call is now — see RtcVoiceState.
start() => Promise<void>Get a single-use call credential with your key (and the customer's identity, when opts.client is set), connect, and place the call; resolves once it is ringing. Rejects with an RtcVoiceError: mint_failed (no key, Graine unreachable — "Could not reach the call service." — or refused, with Graine's message), microphone_denied ("The microphone was not allowed." — with opts.client set, its events hear mic_denied too), connect_failed (the connection failed), already_active ("A call is already in progress." — a second start while one runs).
setMuted(muted: boolean) => voidMute or unmute the customer without ending the call; a mute that fails never ends it.
stop() => Promise<void>Hang up and disconnect; an unfinished start() is cancelled. Does nothing when idle or ended.

Errors — only start() rejects, as above.

const [state, setState] = useState<RtcVoiceState>("idle");
const call = new RtcVoiceSessionController({ publishableKey: "pk_live_…", client, onState: setState, onEnded: () => setOpen(false) });
try { await call.start(); } catch (e) { if (e instanceof RtcVoiceError) showError(e.message); }

RtcVoiceOptions

Read it to configure a WebRTC call: normally only your publishable key and the client.

FieldTypeDefaultMeaning
publishableKeystringnoneThe agent's publishable key — the call is set up with it.
baseUrlstring"https://www.graine.ai"Where Graine is.
getSession() => Promise<RtcVoiceSession>noneOnly for an app that must get the call credential through its own backend: return Graine's answer unread. Without it, publishableKey is required.
onState(s: RtcVoiceState) => voidnoneHear the call's state change — drive your call UI from it.
onEnded(reason: string) => voidnoneHear the call end, with the reason the connection gave ("ended" or "failed" when it gave none).
onLog(line: string) => voidnoneHear diagnostic lines — for your debug log.
sessionIdstringa fresh idTies this call to the client's connection, so the agent sees the screen during the call.
client{ identityChannel(): Promise<IdentityChannel> } | nullnonePass your GraineInAppClient so the call carries who the customer is, this install, the memory choice and the time zone — remembered as the chat is. Without it the call is anonymous.

RtcVoiceSession

Return it, unread, from getSession — it is the call credential Graine issues, and its fields may change.

FieldTypeDefaultMeaning
serverstringrequiredWhere the call connects.
usernamestringrequiredThe single-use credential's name.
passwordstringrequiredThe single-use credential's secret — short-lived; never store it.
realmstringrequiredThe account the credential belongs to.
application_sidstringrequiredWhat the call is placed to.
agent_idstringoptionalThe agent the call is with.
expires_atstringoptionalWhen the credential stops working (ISO-8601).

RtcVoiceState

Read it to draw a WebRTC call's state.

type RtcVoiceState = "idle" | "connecting" | "ringing" | "active" | "ended";
ValueMeaning
"idle"No call yet.
"connecting"Getting the credential and connecting.
"ringing"The call is placed and waiting to be answered.
"active"The call is answered: audio is flowing.
"ended"The call ended or failed; start() may be called again.

useKeyboardLift

Call it in a bottom-anchored view of your own (a bar, a composer) to keep it above the keyboard on both platforms.

function useKeyboardLift(): number

Parameters — none.

Returns — the points to add to the view's bottom while the keyboard is up: the keyboard's height minus however much the window already shrank for it (Android's resize mode), so the view is never lifted twice. 0 while the keyboard is hidden. Example value: 291.

Errors — none.

const lift = useKeyboardLift();
<View style={{ position: "absolute", left: 0, right: 0, bottom: 16 + lift }}>…</View>

liftFor

Use it to compute the keyboard lift yourself, from the keyboard's height and how much the window shrank.

function liftFor(keyboardHeight: number, windowShrink: number): number
ParameterTypeDefaultMeaning
keyboardHeightnumberrequiredHeight of the keyboard according to the platform; 0 while it is hidden.
windowShrinknumberrequiredHow much shorter the window is now than before the keyboard showed.

Returns — keyboardHeight − windowShrink, rounded, never below 0; 0 when the keyboard is hidden. Example value: 0 for a window that already resized.

Errors — none.

liftFor(300, 0);   // 300 — the window did not move (iOS)

useDraggablePlacement

Call it in a bar of your own to let the customer drag it up and down — the gesture, the limits and the arithmetic, with a tap still working.

function useDraggablePlacement(opts: DraggablePlacementOptions): DraggablePlacement
ParameterTypeDefaultMeaning
opts.baseBottomnumberrequiredWhere the bar rests with no offset, in points from the bottom.
opts.enabledbooleantruefalse turns the gesture off; the offset is then always 0.
opts.initialOffsetnumber0A position you saved, to restore; applied when it changes.
opts.onChange(offset: number) => voidnoneHear where the customer let go — save it.
opts.topMarginnumber48Space to keep clear at the top of the screen.

Returns — offset: points to add to the bar's bottom; panHandlers: spread on the bar's outermost view; onLayout: attach to the same view so the limits know the bar's height; dragging: true while a finger moves it (do not open the panel then); reset(): put it back. Example value: { offset: 120, dragging: false, … }.

Errors — none.

const drag = useDraggablePlacement({ baseBottom: 24, onChange: (o) => save("barOffset", o) });
<View {...drag.panHandlers} onLayout={drag.onLayout} style={{ position: "absolute", bottom: 24 + drag.offset }}>…</View>

clampOffset

Use it to keep a bar's offset where the whole bar stays on screen.

function clampOffset(offset: number, b: PlacementBounds): number
ParameterTypeDefaultMeaning
offsetnumberrequiredThe proposed offset, in points above the resting place.
b{ windowHeight: number; baseBottom: number; barHeight: number; topMargin?: number }requiredThe screen's height, where the bar rests, the bar's height (0 before it is measured — a modest bar is assumed) and the space to keep clear at the top (48).

Returns — the offset, rounded, between 0 and the highest it may go. A number that is not finite becomes 0. Example value: 540.

Errors — none.

clampOffset(9999, { windowHeight: 800, baseBottom: 24, barHeight: 64 }); // 664

offsetDuringDrag

Use it to work out a bar's offset while a finger drags it.

function offsetDuringDrag(startOffset: number, dy: number, b: PlacementBounds): number
ParameterTypeDefaultMeaning
startOffsetnumberrequiredThe offset when the drag began.
dynumberrequiredHow far the finger moved down since then (a gesture's own dy; moving up is negative).
bPlacementBoundsrequiredThe limits, as for clampOffset.

Returns — startOffset − dy, kept on screen. Example value: 140.

Errors — none.

offsetDuringDrag(100, -40, { windowHeight: 800, baseBottom: 24, barHeight: 64 }); // 140

isTap

Use it to tell a tap from a drag on something the customer can drag.

function isTap(dx: number, dy: number): boolean
ParameterTypeDefaultMeaning
dxnumberrequiredHow far the finger moved across.
dynumberrequiredHow far it moved up or down.

Returns — true when both moved less than DRAG_SLOP_PX. Example value: true.

Errors — none.

const pan = PanResponder.create({
  onPanResponderRelease: (_, g) => { if (isTap(g.dx, g.dy)) openPanel(); },
});

DRAG_SLOP_PX

Read it to know how far a finger may move and still count as a tap.

const DRAG_SLOP_PX = 8;

What to expect — 8 points, in each direction.

Several agents in one app

Use these when one app runs more than one agent — a support agent and a sales agent, each with its own publishable key, design and conversations.

One provider per agent

Use one GraineProvider per agent: the provider at the root is the app's agent, and a provider inside another's tree is the agent for that part of the app (a sales screen, say).

What to expect — The agents share one device: who the customer is (identify reaches every agent, each reading its own Who is the user? setting), this install's id, the screen the customer is on, and the line — one conversation at a time on the device, whichever agent. Starting a conversation with one agent ends a chat another holds; a call is ended only when the customer starts another conversation themselves (an automatic connection waits for the call instead). Each agent keeps its own key, design, settings, memory and booked calls. Only the provider at the root registers its client as the default for hooks outside every provider.

<GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_support…" storage={AsyncStorage}>
  <RootNavigator />          {/* the support agent everywhere… */}
</GraineProvider>
 
function SalesScreen() {
  return (
    <GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_sales…" storage={AsyncStorage}>
      <SalesContent />         {/* …and the sales agent on this screen */}
    </GraineProvider>
  );
}

For calls that ring, configure every agent in the calls package too — see GraineCalls.configure and GraineCalls.agents.

GraineInAppClient.conversation

Read it to know which conversation this agent's client holds right now, if any — one at a time on the device.

get conversation(): "call" | "chat" | null

Returns — "call": the voice launcher's call, or this client's spoken connection; "chat": this client's typed chat (open, opening, or coming back after a drop); null: none. Example value: "chat".

Errors — none.

if (supportClient.conversation === "call") showReturnToCallBanner();

GraineInAppClient.dispose

Call it when you are done with a client you built yourself — its conversation ends and it stops sharing the device with the app's other clients.

dispose(): void

Parameters — none.

Returns — nothing. The conversation is hung up as close() does, the client leaves the device, and a booking sheet still open for it resolves null.

Errors — none.

useEffect(() => () => salesClient.dispose(), [salesClient]);

GraineInAppClient.joinDevice

The provider calls it as it mounts; a client joins the device when it is built, so your app does not need it.

joinDevice(): void

Parameters — none.

Returns — nothing. The client shares the device again and takes on the customer the app has identified.

Errors — none.

client.joinDevice();

GraineInAppClient.leaveDevice

The provider calls it as it unmounts a client it built; to finish with a client of your own, call dispose().

leaveDevice(): void

Parameters — none.

Returns — nothing. The client stops sharing the device; one still bound to a customer keeps hearing who the customer is until it has signed them out.

Errors — none.

client.leaveDevice();

GraineInAppClient.yieldLine

The SDK calls it when another agent takes the line; your app does not need it.

yieldLine(): void

Parameters — none.

Returns — nothing. A call this client's launcher holds ends as a hang-up does (announced as line_taken, { reason: "another_agent" }), and this client's connection closes (the close event carries reason: "another_agent").

Errors — none.

client.yieldLine();

GraineCalls.agents

Read it to see which agents the calls package has been configured with.

get agents(): string[]

Returns — "default", then the default agent's own name when it was configured under one, then every other agent's name; [] before configure(). Example value: ["default", "support", "sales"].

Errors — none.

if (!GraineCalls.agents.includes("sales")) await GraineCalls.configure("sales", { publishableKey: "pk_live_sales…" });

DEFAULT_AGENT

Read it as the name the calls package gives the agent configure(options) sets up — the one a call uses when you name none.

const DEFAULT_AGENT = "default";

What to expect — Always "default". startCall({ agent: "default" }) is the same as startCall(); a call's agent reads "default" unless you named the default agent yourself.

GraineCallsAgentOptions

Read it to add one more agent to the calls package: its own publishable key, and its own address when it has one.

FieldTypeDefaultMeaning
publishableKeystringrequiredThe agent's publishable key — calls placed to it and rings from it are matched by it. A missing key rejects configure with publishableKey is required for the agent "<name>".
baseUrlstringthe default agent'sWhere this agent's Graine is, when it is not the default agent's.

What to expect — The name on the ring, the ringtone and the call screen's design come from that agent's own dashboard settings (the name falls back to displayName); every other option is the default agent's.

GraineCallsAgentsOptions

Read it to configure several agents in one call without a default key: the first agent listed becomes the default.

type GraineCallsAgentsOptions = Omit<GraineCallsOptions, "publishableKey" | "agents"> & {
  publishableKey?: undefined;
  agents: Record<string, GraineCallsAgentOptions>;
};
FieldTypeDefaultMeaning
publishableKeyundefined—Left out: there is no separate default key. The first agent in agents is the default — the one startCall() uses with no agent named, and the one a ring for an agent this app did not configure comes in as.
agentsRecord<string, GraineCallsAgentOptions>requiredEvery agent by your name for it, each with its own key.

What to expect — Every other field is a GraineCallsOptions field (displayName is still required) and applies to every agent.

await GraineCalls.configure({
  displayName: "Acme",
  agents: { support: { publishableKey: "pk_live_support…" }, sales: { publishableKey: "pk_live_sales…" } },
});

Screen context, actions and components

Use these to tell the agent what the customer can see, record what they do, let the agent act in your app and point at things, draw the components the agent sends, notice a customer who is stuck, mask what must not leave the phone, and decide where the launcher appears.

useGraineScreen

Call it in each screen component to tell the agent what that screen shows — for as long as it is mounted.

function useGraineScreen(context: ScreenContext | null): void
ParameterTypeDefaultMeaning
contextScreenContext | nullrequiredWhat the customer can see on this screen — see ScreenContext; null while there is nothing to say.

Returns — nothing. The screen is re-sent whenever its content changes (compared by value, so an object rebuilt on every render does not resend), and withdrawn on unmount — only if it is still the screen being described, so a screen leaving during a navigation never erases the next one.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

What to expect — With navigationRef on the provider every route is already reported by name; this hook adds the detail a route name cannot carry — the step, the fields, the error showing. Values are masked on the device; an unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms.

useGraineScreen({
  screen: "checkout_address", title: "Delivery address",
  journey: { name: "Checkout", step: 2, of: 3, next: "Payment" },
  fields: [{ name: "postal_code", label: "Postal code", value: postalCode, status: "invalid", error: "We don't deliver there yet" }],
});

ScreenContext

Read it to describe a screen to the agent: what it is, where it sits in a flow, its fields and what may be done on it.

FieldTypeDefaultMeaning
screenstringrequiredA stable id for the screen ("checkout_address") — the per-screen friction rules and the conversation's timeline key on it, so keep it constant.
titlestringoptionalThe screen's title as the customer sees it — the agent says it out loud.
journey{ name: string; step?: number; of?: number; next?: string }optionalWhere the screen sits in a flow: its name, the step and how many there are, and what comes next — so the agent can answer "what happens after this?" instead of guessing.
fieldsScreenField[]optionalThe inputs on the screen and how each is going — see ScreenField.
errorstringoptionalAn error banner for the whole screen, apart from any field's own error.
dataRecord<string, unknown>optionalAnything else the agent should know about the screen — short, shallow values.
availableActionsstring[]optionalThe actions that work on this screen. Leave it out to allow every action the agent declares; it can only narrow that list, never add to it ([] means none here).

ScreenField

Read it to describe one input on the screen — its status is what lets the agent name the field that is wrong.

FieldTypeDefaultMeaning
namestringrequiredThe field's id, unique on the screen — updateField and removeField find it by this.
labelstringoptionalThe label the customer sees — the agent uses it to name the field.
valuestring | number | nulloptionalWhat is in it. Masked before it leaves the device; never send a raw identifier.
status"empty" | "filled" | "invalid" | "pending" | "locked"optionalHow the field is going: empty, filled, invalid (rejected), pending (being checked) or locked (cannot be changed).
errorstringoptionalThe validation message actually showing — the most useful thing the agent can offer to fix.
optionalbooleanoptionaltrue when leaving it blank is fine: it stays on the screen for the agent but is not counted as work left. A field with an error is outstanding either way.

useGraineField

Call it next to one input's own state so the agent sees its value, its status and the error the moment it shows — without re-describing the whole screen on every keystroke.

function useGraineField(name: string, field: Omit<ScreenField, "name">): void
ParameterTypeDefaultMeaning
namestringrequiredThe field's id on the screen.
fieldOmit<ScreenField, "name">requiredIts label, value, status, error and whether it is optional — see ScreenField.

Returns — nothing. The field is merged into the current screen when something about it changes, and removed when the input unmounts. It needs a screen to belong to: while none is reported the field is dropped until it next changes — report the screen first (useGraineScreen, or navigationRef on the provider), or include the field in it.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const [postal, setPostal] = useState("");
const error = postal && !/^\d{6}$/.test(postal) ? "Enter the 6-digit code" : undefined;
useGraineField("postal_code", { label: "Postal code", value: postal, error, status: error ? "invalid" : postal ? "filled" : "empty" });

GraineInAppClient.setScreen

Call it outside React (a screen controller, a navigation listener) to tell the agent what the customer is looking at; in components use useGraineScreen.

setScreen(context: ScreenContext | null): void
ParameterTypeDefaultMeaning
contextScreenContext | nullrequiredThe screen as the customer sees it — see ScreenContext; null forgets it (prefer clearScreen).

Returns — nothing. Every agent on the device is told. The screen event fires at once for a host carrying its own conversation; the frame to Graine goes at once the first time and at most every 400 ms after. A different screen id is a new visit for friction detection and restarts the stuck clock.

Errors — none.

client.setScreen({ screen: "order_detail", title: "Your order", data: { status: "Shipped" } });

GraineInAppClient.clearScreen

Call it when a screen goes away — in its cleanup — to withdraw its description, but only if it is still the one being described.

clearScreen(screenId: string | null | undefined): void
ParameterTypeDefaultMeaning
screenIdstring | null | undefinedrequiredThe id of the screen leaving. When a different screen is described now, nothing changes; with no id, the current screen is cleared.

Returns — nothing.

Errors — none.

useEffect(() => () => client.clearScreen("order_detail"), []);

GraineInAppClient.updateField

Call it when one input changes, to report just that field instead of the whole screen.

updateField(field: ScreenField): void
ParameterTypeDefaultMeaning
fieldScreenFieldrequiredThe field — merged by name into the current screen's fields, added when new.

Returns — nothing. Goes out like any screen report (masked, coalesced), and friction detection sees it — a field rejecting the customer twice is noticed here. With no screen reported yet, nothing happens — the field is not kept for later.

Errors — none.

client.updateField({ name: "email", label: "Email", status: "invalid", error: "Enter a valid email" });

GraineInAppClient.removeField

Call it when an input leaves the screen.

removeField(name: string): void
ParameterTypeDefaultMeaning
namestringrequiredThe field's id. A field that was never reported is ignored.

Returns — nothing.

Errors — none.

client.removeField("promo_code");

GraineInAppClient.getScreen

Read it to see the screen as last reported — to forward it, or to check what the agent was told.

getScreen(): ScreenContext | null

Parameters — none.

Returns — the last ScreenContext exactly as reported (unmasked), or null when none is. Example value: { screen: "checkout_address", title: "Delivery address" }.

Errors — none.

const where = client.getScreen()?.screen ?? "unknown";

GraineInAppClient.buildContextFrame

Call it only when you carry the conversation yourself: it builds the context the agent should have right now — the screen, the customer, recent events — masked.

buildContextFrame(): { type: "app_context"; context: Record<string, unknown>; available_actions?: string[]; seq: number } | null

Parameters — none.

Returns — the frame, or null when there is no screen, no identified customer and no recent event to describe. context holds the screen's fields plus idle_ms (how long since the customer last did anything on it), user (the masked traits), app_version, recent_events and highlightable (what the agent may point at), all masked; available_actions lists the actions usable here — absent means every declared action, [] means none; seq counts frames. Example value: { type: "app_context", context: { screen: "checkout_address", idle_ms: 4200, user: { name: "Asha" } }, seq: 7 }.

Errors — none.

const frame = client.buildContextFrame();
if (frame) myConnection.send(JSON.stringify(frame));

useGraineTrack

Call it to record something that happened in your app — a payment declined, an upload that failed — so the agent knows it when the customer next asks, without making it speak.

function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void

Parameters — none.

Returns — a track(name, data?) function; see GraineInAppClient.track.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const track = useGraineTrack();
onPaymentDeclined((reason) => track("payment_declined", { reason }));

GraineInAppClient.track

Call it outside React to record a product event the agent should know about, without making it speak.

track(name: string, data?: Record<string, unknown>, opts?: { coalesce?: boolean }): void
ParameterTypeDefaultMeaning
namestringrequiredThe event's name — "payment_declined". Names starting tap: are taps (see useGraineTap). An empty name is ignored.
dataRecord<string, unknown>noneDetails of the event; masked on the device.
opts.coalescebooleanfalsetrue for a burst you would rather send once — automatic tap capture uses it: friction still sees each event at once, and the context goes out at the end of the 400 ms window.

Returns — nothing. The last 10 events ride with the screen context (oldest dropped), every agent on the device records them, and the same event recorded twice within a moment by two providers counts once. Friction detection reads taps here (rage taps, a submit with fields still to fill).

Errors — none.

client.track("upload_failed", { file: "photo.jpg", reason: "too_large" });

GraineInAppClient.getRecentEvents

Read it to see the events the agent will be given, oldest first.

getRecentEvents(): Array<{ name: string; at: number; data?: Record<string, unknown> }>

Parameters — none.

Returns — a copy of the last 10 tracked events: name, at (milliseconds since 1970) and the data given. Example value: [{ name: "tap:pay", at: 1760500000000 }].

Errors — none.

console.log(client.getRecentEvents().map((e) => e.name));

useGraineTap

Call it to wrap a button's onPress so the agent knows the button was tapped — your handler runs exactly as before, even if tracking fails.

function useGraineTap<T extends (...args: any[]) => any>(name: string, handler?: T, data?: Record<string, unknown>): (...args: Parameters<T>) => ReturnType<T> | undefined
ParameterTypeDefaultMeaning
namestringrequiredWhat was tapped, in words the agent can use — tracked as tap:<name> ("pay" → tap:pay).
handlerTnoneYour own handler; it runs after the tap is recorded and its result is returned.
dataRecord<string, unknown>noneDetails to record with the tap; masked on the device.

Returns — the wrapped handler, stable across renders. Example value: [Function].

Errors — none: a tracking failure never stops the button.

const onPay = useGraineTap("pay", handlePay);
<Button title="Pay" onPress={onPay} />

UNNAMED_TAP

Read it as the name an automatically captured tap is recorded under.

const UNNAMED_TAP = "tap:screen";

What to expect — Named taps are tap:<name>; a tap GraineTouchCapture saw with no name for the control is tap:screen — "tapped the screen" — so the timeline, the rage-tap rule and the agent all read it sensibly.

GraineTouchCapture

Use it to record every tap in a part of your app with no per-button wrapping — around screens that sit outside the provider (the provider wraps its own children when captureTaps or fab is set).

function GraineTouchCapture(props: GraineTouchCaptureProps): JSX.Element

Props — GraineTouchCaptureProps.

Returns — a view filling its parent, wrapping children. It never takes a touch away from your app.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client; nothing inside it can crash a touch.

What to expect — A short, still touch — one finger, moving at most TAP_MAX_MOVE_PX, lasting at most TAP_MAX_MS — is recorded as tap:screen with where it landed, only while a screen has been reported and (inside a provider) the launcher may show.

<GraineTouchCapture><RootNavigator /></GraineTouchCapture>

GraineTouchCaptureProps

Read it to wrap the part of your app whose taps should be recorded.

PropTypeDefaultMeaning
childrenReact.ReactNoderequiredThe part of your app to watch.
enabledbooleantruefalse stops recording without unmounting the wrapper — on a payment keypad, say.
styleStyleProp<ViewStyle>{ flex: 1 }The wrapper view's style.

TAP_MAX_MOVE_PX

Read it to know how far a touch may move and still count as a tap.

const TAP_MAX_MOVE_PX = 12;

What to expect — 12 points; further is a scroll or a drag.

TAP_MAX_MS

Read it to know how long a touch may last and still count as a tap.

const TAP_MAX_MS = 400;

What to expect — 400 milliseconds; longer is a long-press.

GraineInAppClient.reportEvent

Call it when something happens that the agent may want to speak up about — an upload failed, an application was rejected — and let the agent decide whether to.

reportEvent(event: AppEvent): void
ParameterTypeDefaultMeaning
eventAppEventrequiredWhat happened and how the agent should treat it — see AppEvent.

Returns — nothing. The agent holding the conversation on the device hears it; the app_event event fires as well, for a host carrying the conversation itself.

Errors — none.

client.reportEvent({ name: "upload_failed", detail: "The customer's photo upload failed twice: the file is over 10 MB." });

AppEvent

Read it to describe something worth the agent speaking up about, unprompted — or, with quiet, something it should simply know.

FieldTypeDefaultMeaning
namestringrequiredThe event's name — "upload_failed", "stalled_on_step".
detailstringoptionalThe situation in plain words; the agent is given it as it is.
cooldown_secondsnumber20Seconds before the agent may volunteer again.
max_per_sessionnumber3The most times the agent may volunteer in one conversation.
quietbooleanfalsetrue to record it in the agent's context without making it speak — a state, not a nudge.
dataRecord<string, unknown>optionalStructured details — short, shallow values.

GraineInAppClient.noteInteraction

Call it when the customer does something your screen reports no other way — they are not stuck — to restart the stuck clock.

noteInteraction(): void

Parameters — none.

Returns — nothing. Every agent on the device restarts its idle timer for the current screen. The SDK calls it for you when the customer speaks.

Errors — none.

<ScrollView onScrollBeginDrag={() => client.noteInteraction()}>…</ScrollView>

useGraineVariables

Call it to give the agent's prompt values from your app's own data while it runs — the plan just upgraded to, the cart total.

function useGraineVariables(): (vars: Record<string, unknown> | null) => void

Parameters — none.

Returns — a setVariables(vars) function; see GraineInAppClient.setVariables.

Errors — throws outside a GraineProvider.

const setVariables = useGraineVariables();
useEffect(() => { setVariables({ plan: account.plan, cart_total: cart.total }); }, [account.plan, cart.total]);

GraineInAppClient.setVariables

Call it outside React to set prompt variables while the app runs.

setVariables(vars: Record<string, unknown> | null): void
ParameterTypeDefaultMeaning
varsRecord<string, unknown> | nullrequiredVariables by name. Merged into what you set before; a key set to null is removed; null clears them all. Numbers and booleans become strings; objects are ignored.

Returns — nothing. Values are masked on the device. The set reaches the next conversation on every path — the chat, the voice launcher's next call, the calls package — and a live voice call at once. The variables event fires with the merged set.

Errors — none.

What to expect — Precedence, last wins: the agent's defaults in the dashboard, the provider's variables, these, then the customer's traits from identify(). On the usual ticketed connection the agent receives at most 12 variables, about 1 KB, within the TICKET_VARIABLE_LIMITS — past that, the biggest are left off first, who_called and call_reason last.

client.setVariables({ plan: "Plus" });
client.setVariables({ city: "Pune" });     // both are set now
client.setVariables({ city: null });       // city removed

GraineInAppClient.getVariables

Read it to see every variable the agent will receive — to check what a conversation will carry, or when you carry the conversation yourself.

getVariables(): Record<string, string>

Parameters — none.

Returns — every variable the agent will receive, merged in precedence order — the dashboard's defaults, the provider's variables, setVariables(), then identify() (without the identifying trait) — as the next conversation carries them: the other SDKs' currentVariables(). A copy: changing it changes nothing. The same set as allVariables(). Example value: { first_name: "Asha", plan: "Plus" }.

Errors — none.

const vars = client.getVariables();

GraineInAppClient.allVariables

Read it to see the merged variables in their order of precedence — the same set getVariables() returns.

allVariables(): Record<string, string>

Parameters — none.

Returns — the agent's dashboard defaults, then the provider's variables, then setVariables(), then the customer's masked traits (without the identifying trait) — later ones winning. Example value: { greeting_name: "Asha", plan: "Plus" }.

Errors — none.

console.log(client.allVariables());

useGraineAction

Call it in the component that can do the thing — "open the cart", "switch language" — to let the agent do it while that component is mounted.

function useGraineAction(name: string, handler: ActionHandler): void
ParameterTypeDefaultMeaning
namestringrequiredThe action's name, exactly as declared on the agent (Agent → Embed & Widgets → Actions).
handlerActionHandlerrequiredWhat to do. It receives the agent's arguments and returns ok, refused or error — see ActionHandler.

Returns — nothing. The action is available while the component is mounted and withdrawn when it unmounts. A handler that closes over fresh state is used without registering again.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

useGraineAction("set_language", ({ language }) => {
  setLanguage(String(language));
  return { status: "ok", message: `The app is now in ${language}.` };
});

GraineInAppClient.registerAction

Call it outside React to teach the agent one thing it can do in your app.

registerAction(name: string, handler: ActionHandler): () => void
ParameterTypeDefaultMeaning
namestringrequiredThe action's name, exactly as declared on the agent. Registering a name again replaces its handler.
handlerActionHandlerrequiredWhat to do — see ActionHandler.

Returns — a function that unregisters it. The change reaches the agent with the screen context at once.

Errors — none. An action the agent does not declare is never called — checkActions() lists such mismatches.

const off = client.registerAction("open_cart", () => { navigation.navigate("Cart"); return { status: "ok" }; });

ActionHandler

Read it to write an action handler: it gets the agent's arguments and answers what happened.

type ActionHandler = (args: Record<string, any>) => AppActionResult | void | Promise<AppActionResult | void>;

What to expect — Answer within about 5.5 seconds: after that the agent is told ""<name>" did not finish in time.", and after 6 seconds it would decide the action did not happen. For slow work, return at once and report the result with reportEvent or the screen when it is done. Returning nothing counts as { status: "ok" }; a throw is answered error with ""<name>" failed: <message>".

AppActionResult

Read it to answer an action honestly: ok makes the agent tell the customer it is done.

FieldTypeDefaultMeaning
status"ok" | "error" | "refused"requiredok — it happened; refused — the customer or the app declined (with a reason, the most useful answer there is); error — it failed.
dataRecord<string, unknown>optionalStructured results the agent can reason about.
messagestringoptionalA sentence for the agent, not the customer — it decides how to put it.

GraineInAppClient.executeAction

Call it to run one of your registered actions yourself and get its answer — the calls package uses it to run the agent's actions during a call.

async executeAction(name: string, args: Record<string, unknown> = {}): Promise<AppActionResult>
ParameterTypeDefaultMeaning
namestringrequiredThe registered action's name.
argsRecord<string, unknown>{}Its arguments.

Returns — the handler's AppActionResult, within the same deadline as an action the agent asks for. An action this build never registered answers refused, with the message This app build does not implement "open_cart". for an action named open_cart. The action_result event fires either way. Example value: { status: "ok", message: "The app is now in Hindi." }.

Errors — none: a throw or a timeout is answered error, never rejected.

const result = await client.executeAction("set_language", { language: "hi" });

AppActionRequest

Read it to see what the agent sends when it asks your app to act — the action event carries it.

FieldTypeDefaultMeaning
action_idstringrequiredThe request's id; the answer is matched to it.
namestringrequiredThe action's name — matches a handler you registered.
kindstringoptionalThe kind of action, when the agent says.
argumentsRecord<string, unknown>requiredThe agent's arguments for the action.

GraineInAppClient.checkActions

Call it while developing to see which of your actions the agent can really call — an action must be both registered in the app and declared on the agent.

checkActions(): ActionDiagnosis

Parameters — none.

Returns — an ActionDiagnosis. Before the agent's settings load, every registered action reads as undeclared. Example value: { usable: ["open_cart"], undeclared: ["set_theme"], unhandled: ["open_orders"] }.

Errors — none. When the settings load with a mismatch, the console says so and the diagnostic event fires.

const { undeclared, unhandled } = client.checkActions();

ActionDiagnosis

Read it to fix your actions: what works, what the agent cannot call, and what your app does not handle.

FieldTypeDefaultMeaning
usablestring[]requiredRegistered here and declared on the agent — these work.
undeclaredstring[]requiredRegistered here but not declared on the agent — the agent cannot call them; declare them in Embed & Widgets.
unhandledstring[]requiredDeclared on the agent with no handler here — the agent will be told this build cannot do them.

GraineInAppClient.availableActions

Read it to see which actions this build can perform right now.

get availableActions(): string[]

Returns — the names of the registered actions. Example value: ["open_cart", "set_language"].

Errors — none.

console.log(client.availableActions);

GraineInAppClient.getAvailableActions

Read it for the same list as a fresh copy — the calls package reads it.

getAvailableActions(): string[]

Parameters — none.

Returns — a copy of the registered actions' names. Example value: ["open_cart"].

Errors — none.

const names = client.getAvailableActions();

Register a handler under this name once, and every screen a component opens can be set in the dashboard with no app release.

const NAVIGATE_ACTION = "open_screen";

What to expect — A component's tap with a path runs this action with { path }; an app that has not registered it refuses, and the agent is told.

useGraineAction(NAVIGATE_ACTION, ({ path }) => { navigation.navigate(String(path)); return { status: "ok" }; });

useGraineHighlight

Call it on an element the agent may point at — "tap Continue" is no help if the customer cannot find Continue — and style it while highlighted is true.

function useGraineHighlight(name: string): { highlighted: boolean }
ParameterTypeDefaultMeaning
namestringrequiredThe name the agent points at it by — "continue_button".

Returns — highlighted: true while the agent is pointing at it (6 seconds by default). The SDK draws nothing: style it as your app would — a ring, a pulse. Example value: { highlighted: false }.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

What to expect — Declare the highlight_element action on the agent once; the SDK answers it itself, refusing names that are not on screen and naming the ones that are.

const { highlighted } = useGraineHighlight("continue_button");
<Pressable style={[styles.button, highlighted && styles.glow]}>…</Pressable>

GraineInAppClient.registerHighlightable

Call it outside React to make an element something the agent may point at.

registerHighlightable(name: string): () => void
ParameterTypeDefaultMeaning
namestringrequiredThe element's name; an empty name is ignored.

Returns — a function that withdraws it. The agent is offered it with the screen context.

Errors — none.

const off = client.registerHighlightable("plan_card");

GraineInAppClient.getHighlightables

Read it to see what the agent can point at right now.

getHighlightables(): string[]

Parameters — none.

Returns — the registered names. Example value: ["continue_button", "plan_card"].

Errors — none.

console.log(client.getHighlightables());

GraineInAppClient.highlight

Call it to point at an element yourself — the agent's highlight_element comes through here too.

highlight(name: string | null, ttlMs = 6000): void
ParameterTypeDefaultMeaning
namestring | nullrequiredThe element to point at; null to stop.
ttlMsnumber6000How long the highlight lasts, in milliseconds.

Returns — nothing. The highlight event fires with { name, ttlMs }; useGraineHighlight follows it.

Errors — none.

client.highlight("continue_button", 4000);

useGraineWidget

Call it to draw the component the agent just sent — a product list, choices, time slots, a card — yourself, and to answer it; GraineWidget does all of this for you.

function useGraineWidget(): {
  widget: GraineWidget | null;
  answered: { widget: GraineWidget; data: Record<string, unknown>; summary: string } | null;
  loading: { id: string; kind: string } | null;
  awaitingReply: boolean;
  closed: GraineWidget | null;
  reopen: () => void;
  submit: (data: Record<string, unknown>, summary: string) => void;
  dismiss: () => void;
}

Parameters — none.

Returns — the component's state and controls:

FieldTypeMeaning
widgetGraineWidget | nullThe component on screen now (see the object below), or null. A component sent while nothing was listening is kept until it is answered.
answered{ widget: GraineWidget; data: Record<string, unknown>; summary: string } | nullWhat the customer just chose, so it can stay on screen as one line; cleared when the next component arrives or on dismiss.
loading{ id: string; kind: string } | nullA component is on its way — draw a skeleton for kind; cleared when it arrives, or after PENDING_WIDGET_MS.
awaitingReplybooleanThe customer answered and the agent has not spoken since — draw the loader beside their choice; cleared when the agent starts, or after AWAITING_REPLY_MS.
closedGraineWidget | nullThe most recent component the customer dismissed without picking anything.
reopen() => voidPut closed back on screen, exactly as it was.
submit(data: Record<string, unknown>, summary: string) => voidAnswer the component: data is the structured answer, summary the sentence the agent hears (the customer's own bubble in a chat). When the component names an in-app action or path, that runs first (a failure is told to the agent) and its say replaces the summary.
dismiss() => voidClose it without choosing. The agent is told quietly, so it does not refer to options the customer can no longer see.

The component object — what widget, closed and answered.widget hold:

FieldTypeMeaning
idstringThe component's id — submitWidget answers it by this.
kindstringWhat to draw — "product_list", "choices", "calendar_slots", "info_card", or whatever your agent declares. Draw fallbackText for a kind you cannot draw.
dataRecord<string, unknown>The content to draw it from; image paths are already absolute addresses.
fallbackTextstringWhat the agent would have said instead — show it when you cannot draw kind.
styleWidgetStyleHow the dashboard asked for it to look, already checked — see WidgetStyle.
onSelectWidgetSelectActionWhat your app should do on a tap, beyond telling the agent — see WidgetSelectAction.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const { widget, submit, dismiss } = useGraineWidget();
if (widget?.kind === "choices") return <Choices items={widget.data.items as Array<{ id: string; title: string }>} onPick={(i) => submit({ id: i.id }, `I'll take ${i.title}`)} onClose={dismiss} />;

GraineWidget

Mount it where you want the agent's components drawn by the SDK — a sheet, a full screen or a card above the bar, in the agent's design — and the SDK's own bars leave components to it.

function GraineWidget(props: GraineWidgetProps): JSX.Element | null

Props — GraineWidgetProps.

Returns — the current component (or its loader, or the customer's answered choice), or null when there is none.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client. A component that fails to draw draws nothing; the conversation goes on.

What to expect — Where it draws follows widgetSurface(kind) unless the dashboard chose: product lists take the screen; comparisons, choices, quick actions and time slots slide up as a sheet; an information card, or a kind this release cannot draw, sits as a card. Spoken prompts ("just say it") show only on a call. Light or dark follows the dashboard, and "auto" follows the phone.

<GraineWidget />

GraineWidgetProps

Read it only to override the dashboard's look for components in your app — every prop is optional.

PropTypeDefaultMeaning
themePartial<GraineWidgetTheme>the dashboard's light or dark paletteColours to use instead, one at a time — see GraineWidgetTheme.
accentstringthe component's, else the dashboard's accentThe colour of buttons and badges.
radiusnumberthe component's, else the dashboard'sThe corner rounding, in points.

GraineWidgetTheme

Read it to override the colours a component is drawn in.

FieldTypeDefaultMeaning
surfacestringthe palette'sThe component's background.
borderstringthe palette'sHairlines and chip fills.
textstringthe palette'sTitles and prices.
mutedstringthe palette'sSubtitles, facts and secondary labels.
onAccentstringchosen for contrastText on the accent — button labels. Chosen by brightness, so a pale accent still reads.

GraineInAppClient.submitWidget

Call it to answer a component the agent drew, when you draw components yourself without useGraineWidget.

submitWidget(widgetId: string, data: Record<string, unknown>, summary: string): void
ParameterTypeDefaultMeaning
widgetIdstringrequiredThe component's id.
dataRecord<string, unknown>requiredThe structured answer — what was chosen.
summarystringrequiredThe sentence the agent hears as the customer's turn — "I'll take the Plus plan". Without it the agent sees the answer but the conversation does not move.

Returns — nothing. The widget_result event fires for a host carrying the conversation itself.

Errors — none.

const shown = client.pendingWidget;   // the component as the agent sent it
if (shown) client.submitWidget(shown.widget_id, { plan: "plus" }, "I'll take the Plus plan");

GraineInAppClient.pendingWidget

Read it to find a component the agent sent while your UI was not mounted — so the customer never opens your panel to nothing.

get pendingWidget(): any

Returns — the component as the agent sent it (with widget_id, kind, data, fallback_text), kept until it is answered or dismissed; null when there is none. useGraineWidget reads it for you. Example value: null.

Errors — none.

if (client.pendingWidget) openAgentPanel();

GraineInAppClient.clearPendingWidget

Call it when a component you drew yourself has been answered or dismissed, so it is not shown again.

clearPendingWidget(): void

Parameters — none.

Returns — nothing.

Errors — none.

client.clearPendingWidget();

GraineInAppClient.presentWidget

Use it to draw a component yourself — a preview, a test, a screen of your own — as if the agent had just sent it.

presentWidget(frame: Record<string, unknown>): void
ParameterTypeDefaultMeaning
frameRecord<string, unknown>requiredThe component as the agent sends it: widget_id (required — a frame without it is not drawn), kind, data, and optionally fallback_text, style and on_select.

Returns — nothing. It becomes the pending component (pendingWidget) and every component view draws it — useGraineWidget(), GraineWidget, the bar — announced as the widget event.

Errors — none.

What to expect — It is read exactly as the agent's would be: image paths in data become absolute addresses, and style and on_select are checked. An answer (submit, submitWidget) goes to the conversation that is live, as for a component the agent sent.

client.presentWidget({
  widget_id: "preview_plans", kind: "choices", fallback_text: "Plus or Pro?",
  data: { items: [{ id: "plus", title: "Plus" }, { id: "pro", title: "Pro" }] },
});

GraineInAppClient.presentPendingWidget

Use it to show the loader a component on its way draws — to preview the dashboard's loader, or in a test — as if the agent had asked for one.

presentPendingWidget(hint: Record<string, unknown> = {}): void
ParameterTypeDefaultMeaning
hintRecord<string, unknown>{}What is coming: kind, for its skeleton, and optionally widget_id.

Returns — nothing. Announced as widget_pending: useGraineWidget().loading and GraineWidget draw the loader until a component arrives (presentWidget) or PENDING_WIDGET_MS passes.

Errors — none.

client.presentPendingWidget({ kind: "product_list" });
setTimeout(() => client.presentWidget({ widget_id: "preview_list", kind: "product_list", data: { items: [] } }), 1500);

PENDING_WIDGET_MS

Read it to know how long a "component coming" skeleton may stand with nothing arriving.

const PENDING_WIDGET_MS = 12_000;

What to expect — 12 seconds; useGraineWidget().loading clears itself after it.

AWAITING_REPLY_MS

Read it to know how long the loader beside an answered component waits for the agent.

const AWAITING_REPLY_MS = 12_000;

What to expect — 12 seconds; useGraineWidget().awaitingReply clears itself after it, so a reply that never comes never leaves a spinner.

widgetSurface

Use it to decide where a component of a kind is drawn, by the same rule as the SDK and the website.

function widgetSurface(kind: string | null | undefined): WidgetSurface
ParameterTypeDefaultMeaning
kindstring | null | undefinedrequiredThe component's kind.

Returns — "screen" for product_list and products; "sheet" for plan_comparison, choices, quick_actions and calendar_slots; "card" for info_card and every kind it does not know (what is drawn for those is the agent's sentence, which should not take over the screen). Example value: "sheet".

Errors — none.

widgetSurface("calendar_slots"); // "sheet"

WidgetSurface

Read it as where a component goes on screen.

type WidgetSurface = "screen" | "sheet" | "card";
ValueMeaning
"screen"The component fills the whole screen — for pictures or several items to compare.
"sheet"It slides over the app — a choice to make, then back.
"card"It sits above the bar — a fact, or a kind that cannot be drawn.

WIDGET_SURFACE_CHOICES

Use it to list the surface choices the dashboard offers for a component.

const WIDGET_SURFACE_CHOICES: ReadonlyArray<"auto" | WidgetSurface> = ["auto", "screen", "sheet", "card"];

What to expect — "auto" keeps the per-kind rule of widgetSurface.

WidgetStyle

Read it to see how the dashboard asked for a component to look — the preset and the adjustments on it.

What to expect — Already checked when it reaches you: unknown keys and values outside these lists are dropped, so an older SDK draws the default for a preset it does not know.

FieldTypeDefaultMeaning
presetWidgetPreset"cards"The whole look in one word — see WidgetPreset.
imageSizeWidgetImageSizethe preset'sHow big pictures are drawn.
densityWidgetDensity"comfortable"How much room around each item.
buttonWidgetButtonthe preset'sHow the action buttons are drawn.
factsbooleantrueShow the fact chips.
columns1 | 21Columns, for the gallery and cards presets only.
surface"auto" | WidgetSurface"auto"Where it is drawn; "auto" follows widgetSurface.
radiusnumberthe Appearance radiusCorner rounding, 0–24.
accentstringthe Appearance accentA #RRGGBB colour for this component.
platformWidgetPlatform"auto"Whose design conventions to follow — see WidgetPlatform.
selectWidgetSelect"one"With "many", the customer can choose several options and send a single answer.
maxSelectnumber3With select: "many", the most that may be picked, 2–8.

WidgetLook

Read it as the look to draw: the preset's defaults with the adjustments on top, every field settled.

FieldTypeDefaultMeaning
presetWidgetPresetrequiredThe preset in force.
imageSizeWidgetImageSizerequiredThe picture size in force (gallery and spotlight large, cards medium, list and compact small, unless chosen).
densityWidgetDensityrequiredThe spacing in force.
buttonWidgetButtonrequiredThe button style in force (list row, compact outline, the others filled, unless chosen).
factsbooleanrequiredWhether to show fact chips.
columns1 | 2requiredColumns in force — 2 only for gallery or cards with two chosen.
surfaceWidgetSurfacerequiredWhere to draw it, with "auto" resolved for the kind.
radiusnumberoptionalThe component's own corner rounding, when it has one.
accentstringoptionalThe component's own accent, when it has one.
platformWidgetPlatformrequiredAs chosen; "auto" is for the renderer to resolve with resolvePlatform.
selectWidgetSelectrequiredOne answer, or several.
maxSelectnumberrequiredThe most that may be picked — meaningful only with select: "many".

normaliseWidgetStyle

Use it to check a style that came from anywhere: only the keys and values a renderer reads survive.

function normaliseWidgetStyle(raw: unknown): WidgetStyle
ParameterTypeDefaultMeaning
rawunknownrequiredThe style as it arrived.

Returns — a WidgetStyle with unknown keys dropped, words lowercased and checked against their lists, radius clamped to 0–24, accent kept only as #RRGGBB (uppercased), maxSelect clamped to 2–8; {} for anything that is not an object. Example value: { preset: "gallery", columns: 2 }.

Errors — none.

normaliseWidgetStyle({ preset: "Gallery", radius: 40, colour: "red" }); // { preset: "gallery", radius: 24 }

resolveWidgetLook

Use it to settle how a component of a kind should look, from its style.

function resolveWidgetLook(style: WidgetStyle | null | undefined, kind: string | null | undefined): WidgetLook
ParameterTypeDefaultMeaning
styleWidgetStyle | null | undefinedrequiredThe component's style (checked again here).
kindstring | null | undefinedrequiredIts kind, for the surface when the style says "auto".

Returns — a WidgetLook. Example value: { preset: "cards", imageSize: "medium", density: "comfortable", button: "filled", facts: true, columns: 1, surface: "sheet", platform: "auto", select: "one", maxSelect: 3 }.

Errors — none.

const { widget } = useGraineWidget();
const look = resolveWidgetLook(widget?.style, widget?.kind);

WidgetPreset

Read it as the whole look of a component in one word, chosen on the component in the dashboard.

type WidgetPreset = "gallery" | "cards" | "list" | "spotlight" | "compact";
ValueMeaning
"gallery"Large pictures, filled buttons; one or two columns.
"cards"Medium pictures in cards, filled buttons — the default.
"list"Small pictures in rows, each row a button.
"spotlight"One large item at a time, paged.
"compact"Small pictures, outline buttons — the least room.

WIDGET_PRESETS

Use it to list the presets.

const WIDGET_PRESETS: ReadonlyArray<WidgetPreset> = ["gallery", "cards", "list", "spotlight", "compact"];

What to expect — A fixed list.

DEFAULT_WIDGET_PRESET

Read it as the preset a component without one is drawn in.

const DEFAULT_WIDGET_PRESET: WidgetPreset = "cards";

What to expect — "cards".

WidgetImageSize

Read it as how big a component's pictures are drawn.

type WidgetImageSize = "small" | "medium" | "large";
ValueMeaning
"small"Thumbnails beside the text.
"medium"Pictures that share the card with the text.
"large"Pictures that lead the card.

WIDGET_IMAGE_SIZES

Use it to list the picture sizes.

const WIDGET_IMAGE_SIZES: ReadonlyArray<WidgetImageSize> = ["small", "medium", "large"];

What to expect — A fixed list.

WidgetDensity

Read it as how much room a component leaves around each item.

type WidgetDensity = "comfortable" | "compact";
ValueMeaning
"comfortable"The default spacing.
"compact"Tighter, to fit more on screen.

WIDGET_DENSITIES

Use it to list the densities.

const WIDGET_DENSITIES: ReadonlyArray<WidgetDensity> = ["comfortable", "compact"];

What to expect — A fixed list.

WidgetButton

Read it as how a component's action buttons are drawn.

type WidgetButton = "filled" | "outline" | "row";
ValueMeaning
"filled"A solid button in the accent.
"outline"An outlined button.
"row"The whole row is the button.

WIDGET_BUTTONS

Use it to list the button styles.

const WIDGET_BUTTONS: ReadonlyArray<WidgetButton> = ["filled", "outline", "row"];

What to expect — A fixed list.

WidgetPlatform

Read it as whose design conventions a component follows.

type WidgetPlatform = "auto" | "ios" | "material" | "oneui";
ValueMeaning
"auto"Follow the phone: Apple's on an iPhone, Material on Android, One UI on a Samsung.
"ios"Apple's conventions everywhere.
"material"Google's Material conventions everywhere.
"oneui"Samsung's One UI conventions everywhere.

WIDGET_PLATFORMS

Use it to list the platform choices.

const WIDGET_PLATFORMS: ReadonlyArray<WidgetPlatform> = ["auto", "ios", "material", "oneui"];

What to expect — A fixed list.

PlatformGrammar

Read it as the shapes a platform's conventions change — not the colours, which stay the brand's.

FieldTypeDefaultMeaning
radiusnumberrequiredThe card corner rounding the convention would choose, in points — used only when the platform is chosen explicitly (under "auto" the brand's radius wins).
chipRadiusnumberrequiredChip corner rounding (999 is a full pill).
buttonRadiusnumberrequiredButton corner rounding (999 is a full pill).
pillButtonsbooleanrequiredWhether buttons are full pills.
separatorInsetnumberrequiredHow far list dividers are indented, in points (0 runs edge to edge).
grabber{ width: number; height: number }requiredThe size of a sheet's drag handle, in points.
titleWeight"600" | "700" | "500"requiredThe weight of titles.
padBoostnumberrequiredExtra padding, in points.
chipOutlinedbooleanrequiredWhether chips are outlined rather than filled.

PLATFORM_GRAMMAR

Use it to read each platform's conventions as numbers.

const PLATFORM_GRAMMAR: Record<"ios" | "material" | "oneui", PlatformGrammar>;
KeyValueMeaning
ios{ radius: 12, chipRadius: 8, buttonRadius: 12, pillButtons: false, separatorInset: 16, grabber: { width: 36, height: 5 }, titleWeight: "600", padBoost: 0, chipOutlined: false }Apple's: 12-point cards and buttons, 8-point tags, inset dividers, semibold titles.
material{ radius: 16, chipRadius: 8, buttonRadius: 20, pillButtons: true, separatorInset: 0, grabber: { width: 32, height: 4 }, titleWeight: "500", padBoost: 0, chipOutlined: true }Material 3: 16-point cards, outlined chips, pill buttons, full-width dividers, medium titles.
oneui{ radius: 26, chipRadius: 999, buttonRadius: 999, pillButtons: true, separatorInset: 0, grabber: { width: 40, height: 4 }, titleWeight: "700", padBoost: 2, chipOutlined: false }One UI: 26-point cards, pill chips and buttons, roomier padding, bold titles.

resolvePlatform

Use it to turn "auto" into the phone's own conventions.

function resolvePlatform(platform: WidgetPlatform | undefined, os: string | undefined, manufacturer?: string | null): "ios" | "material" | "oneui"
ParameterTypeDefaultMeaning
platformWidgetPlatform | undefinedrequiredThe chosen platform; anything but "auto" is returned as it is.
osstring | undefinedrequiredPlatform.OS.
manufacturerstring | nullnoneThe phone's maker (Android) — a Samsung follows One UI.

Returns — "material" on Android ("oneui" on a Samsung), "ios" everywhere else. Example value: "material".

Errors — none.

const { widget } = useGraineWidget();
const look = resolveWidgetLook(widget?.style, widget?.kind);
resolvePlatform(look.platform, Platform.OS, Platform.OS === "android" ? Platform.constants.Manufacturer : null);

WidgetSelect

Read it as whether the customer picks one item or several.

type WidgetSelect = "one" | "many";
ValueMeaning
"one"A tap answers at once.
"many"Each row gets a tick and one button answers with everything picked — for "compare these".

WIDGET_SELECTS

Use it to list the selection choices.

const WIDGET_SELECTS: ReadonlyArray<WidgetSelect> = ["one", "many"];

What to expect — A fixed list.

MAX_SELECT_DEFAULT

Read it as how many items may be picked when the component says none.

const MAX_SELECT_DEFAULT = 3;

What to expect — 3.

manySummary

Use it to word the answer when several items were picked — the sentence the agent hears and, in a chat, the customer's bubble.

function manySummary(titles: string[], label = "Compare"): string
ParameterTypeDefaultMeaning
titlesstring[]requiredThe names of the items picked.
labelstring"Compare"The button's label, used as it is; an empty label leaves just the names.

Returns — the label, a colon, then the names joined with commas and "and"; "" when nothing was picked. Example value: "Compare: Basic, Plus and Pro".

Errors — none.

manySummary(["Morning", "Evening"], "Taken"); // "Taken: Morning and Evening"

WidgetSelectAction

Read it as what your app does when the customer taps a component's item — beyond telling the agent: open a screen, or run one of your actions.

FieldTypeDefaultMeaning
pathstringoptionalWhere to go, as your app understands it — "/product/{id}" — run through your NAVIGATE_ACTION handler, so new screens are set in the dashboard with no release.
actionstringoptionalOne of your registered actions to run instead ([a-z0-9_]). It wins over path.
argsRecord<string, string>optionalIts arguments. Values may use {id}, {title}, {price}, {subtitle} and {summary}, filled from the tapped item.
saystringoptionalWhat the agent is told instead of the usual sentence; the same placeholders.

normaliseSelectAction

Use it to check a tap action that came from anywhere.

function normaliseSelectAction(raw: unknown): WidgetSelectAction
ParameterTypeDefaultMeaning
rawunknownrequiredThe action as it arrived.

Returns — a WidgetSelectAction: action kept only when it is a valid action name (lowercased); path and say trimmed to SELECT_VALUE_MAX characters; at most SELECT_ARG_MAX arguments, each value cut to SELECT_VALUE_MAX; {} when there is neither an action nor a path. Example value: { path: "/product/{id}" }.

Errors — none.

normaliseSelectAction({ action: "Open_Cart", args: { from: "chat" } }); // { action: "open_cart", args: { from: "chat" } }

fillSelectArgs

Use it to fill a tap action's arguments from the tapped item.

function fillSelectArgs(args: Record<string, string> | undefined, item: Record<string, unknown>): Record<string, string>
ParameterTypeDefaultMeaning
argsRecord<string, string> | undefinedrequiredThe arguments, with placeholders.
itemRecord<string, unknown>requiredThe tapped item's values.

Returns — each argument with its placeholders filled by fillSelectTemplate. Example value: { sku: "P-1001" }.

Errors — none.

fillSelectArgs({ sku: "{id}" }, { id: "P-1001" }); // { sku: "P-1001" }

fillSelectTemplate

Use it to fill {id}, {title} and the other placeholders of one string from the tapped item.

function fillSelectTemplate(template: string, item: Record<string, unknown>): string
ParameterTypeDefaultMeaning
templatestringrequiredThe text with placeholders.
itemRecord<string, unknown>requiredThe tapped item's values.

Returns — the text with each placeholder replaced; a placeholder the item lacks becomes empty, never left as written. Example value: "/product/P-1001".

Errors — none.

fillSelectTemplate("/product/{id}", { id: "P-1001" });

selectCall

Use it to work out the action to run, and its arguments, for one tap on a component's item.

function selectCall(onSelect: WidgetSelectAction | undefined | null, item: Record<string, unknown>): { name: string; args: Record<string, string> } | null
ParameterTypeDefaultMeaning
onSelectWidgetSelectAction | undefined | nullrequiredThe component's tap action.
itemRecord<string, unknown>requiredThe tapped item.

Returns — { name, args }: the explicit action, else NAVIGATE_ACTION; the filled arguments, plus path (filled) whenever there is one. null when the component names neither. Example value: { name: "open_screen", args: { path: "/product/P-1001" } }.

Errors — none.

const { widget } = useGraineWidget();
const item = { id: "plus", name: "Plus" };   // what the customer tapped
const call = selectCall(widget?.onSelect, item);
if (call) await client.executeAction(call.name, call.args);

SELECT_ARG_MAX

Read it as the most arguments a tap action keeps.

const SELECT_ARG_MAX = 10;

What to expect — 10; more are dropped.

SELECT_VALUE_MAX

Read it as the longest a tap action's path, say or argument value may be.

const SELECT_VALUE_MAX = 200;

What to expect — 200 characters; longer is cut.

resolveWidgetImages

Use it to turn a component's image paths into addresses a phone can load, when you draw components yourself (useGraineWidget already does).

function resolveWidgetImages(data: Record<string, unknown>, baseUrl: string): Record<string, unknown>
ParameterTypeDefaultMeaning
dataRecord<string, unknown>requiredThe component's data.
baseUrlstringrequiredThe agent's address — client.opts.baseUrl.

Returns — a copy in which every image field (image, imageUrl, icon, logo, photo, thumbnail and their Url/_url forms), at the top level and one level into lists, that starts with a single / is prefixed with baseUrl. Absolute addresses and data URIs are left alone. Example value: { image: "https://www.graine.ai/uploads/plan-a.png" }.

Errors — none.

resolveWidgetImages({ image: "/uploads/plan-a.png" }, client.opts.baseUrl);

resolveWidgetPalette

Use it to draw something of your own beside a component in the same colours — light, dark, or the phone's choice, as the dashboard says.

function resolveWidgetPalette(appearance: Record<string, any> | null | undefined, opts?: { scheme?: "light" | "dark" | null; accent?: string }): WidgetPalette
ParameterTypeDefaultMeaning
appearanceRecord<string, any> | null | undefinedrequiredThe agent's appearance (useGraineAgent().appearance).
opts.scheme"light" | "dark" | nullnoneThe phone's colour scheme (useColorScheme()), for the dashboard's "auto".
opts.accentstringnoneThe accent, so onAccent is chosen against it.

Returns — a WidgetPalette. Example value: { surface: "#FFFFFF", border: "#E5E4E0", text: "#101318", muted: "#5F6672", onAccent: "#FFFFFF", mode: "light" }.

Errors — none.

const { appearance } = useGraineAgent();
const palette = resolveWidgetPalette(appearance, { scheme: useColorScheme(), accent: "#318CE7" });

WidgetPalette

Read it as the colours a component is drawn in, for the light or dark mode in force.

FieldTypeDefaultMeaning
surfacestringrequiredThe background (#FFFFFF light, #17181B dark).
borderstringrequiredHairlines and chip fills.
textstringrequiredTitles and prices.
mutedstringrequiredSubtitles, facts, secondary labels.
onAccentstringrequiredText on the accent — dark on a bright accent, white otherwise.
modeWidgetModerequiredWhich set this is, with "auto" resolved against the phone.

WidgetMode

Read it as which colour set a palette is.

type WidgetMode = "light" | "dark";
ValueMeaning
"light"The light set.
"dark"The dark set.

LoaderKind

Read it as how waiting looks — chosen in Embed & Widgets → Appearance and drawn by GraineLoader.

type LoaderKind = "dots" | "pulse" | "ring" | "bars" | "mark";
ValueMeaning
"dots"Three dots — the default.
"pulse"A pulsing disc.
"ring"A spinning ring on a faint track.
"bars"Rising and falling bars.
"mark"The brand's logo, breathing.

LOADER_KINDS

Use it to list the loader kinds.

const LOADER_KINDS: ReadonlyArray<LoaderKind> = ["dots", "pulse", "ring", "bars", "mark"];

What to expect — A fixed list.

DEFAULT_LOADER_KIND

Read it as the loader drawn when the dashboard chose none.

const DEFAULT_LOADER_KIND: LoaderKind = "dots";

What to expect — "dots".

LoaderSize

Read it as the loader's size setting.

type LoaderSize = "small" | "medium" | "large";
ValueMeaning
"small"14 points.
"medium"20 points — the default.
"large"28 points.

LOADER_SIZES

Use it to list the loader sizes.

const LOADER_SIZES: ReadonlyArray<LoaderSize> = ["small", "medium", "large"];

What to expect — A fixed list.

LoaderSpeed

Read it as the loader's pace.

type LoaderSpeed = "calm" | "normal" | "quick";
ValueMeaning
"calm"One cycle every 1.6 seconds.
"normal"One cycle every 1.1 seconds — the default.
"quick"One cycle every 0.7 seconds.

LOADER_SPEEDS

Use it to list the loader speeds.

const LOADER_SPEEDS: ReadonlyArray<LoaderSpeed> = ["calm", "normal", "quick"];

What to expect — A fixed list.

LoaderMoment

Read it as the moments a customer waits, each with its own words.

type LoaderMoment = "connecting" | "reconnecting" | "thinking" | "component";
ValueMeaning
"connecting"The conversation is being set up.
"reconnecting"The voice engine is reloading after a failure.
"thinking"The customer has stopped talking and the agent's reply has not begun.
"component"The agent requested a component, and its content is still loading.

LOADER_MOMENTS

Use it to list the moments.

const LOADER_MOMENTS: ReadonlyArray<LoaderMoment> = ["connecting", "reconnecting", "thinking", "component"];

What to expect — A fixed list.

LoaderText

Read it as the words shown for each moment.

FieldTypeDefaultMeaning
connectingstring"Connecting…"Shown while the conversation is set up.
reconnectingstring"Reconnecting…"Shown while the voice engine reloads.
thinkingstring"Thinking…"Shown while the agent works out its answer.
componentstring"Loading…"Shown while a component's content loads.

What to expect — At most LOADER_TEXT_MAX characters each; an empty value means the default.

DEFAULT_LOADER_TEXT

Read it as the words for each moment when the dashboard wrote none.

const DEFAULT_LOADER_TEXT: Required<LoaderText>;
KeyValueMeaning
connecting"Connecting…"While the conversation is set up.
reconnecting"Reconnecting…"While the voice engine reloads.
thinking"Thinking…"While the agent works out its answer.
component"Loading…"While a component's content loads.

LOADER_TEXT_MAX

Read it as the longest a loader's words may be.

const LOADER_TEXT_MAX = 40;

What to expect — 40 characters; longer words are cut.

LoaderStyle

Read it as the loader the dashboard chose, as it arrives with the agent's appearance (appearance.loader).

FieldTypeDefaultMeaning
kindLoaderKind"dots"The animation.
colorstring"accent""accent", or a #RRGGBB colour.
sizeLoaderSize"medium"The size.
speedLoaderSpeed"normal"The pace.
textLoaderTextthe defaultsThe words for each moment.
skeletonbooleanfalseAlso draw the outline of the coming component under the loader — for a list that always looks the same.

LoaderLook

Read it as the loader to draw, as numbers a renderer uses directly.

FieldTypeDefaultMeaning
kindLoaderKindrequiredThe animation to draw.
colorstring | nullrequiredThe colour; null means the accent.
pxnumberrequiredThe mark's size, in points (14, 20 or 28).
periodMsnumberrequiredOne animation cycle, in milliseconds (1600, 1100 or 700).
textRequired<LoaderText>requiredThe words for every moment, defaults filled.
skeletonbooleanrequiredWhether to draw the coming component's outline too.

normaliseLoaderStyle

Use it to check a loader style that came from anywhere.

function normaliseLoaderStyle(raw: unknown): LoaderStyle
ParameterTypeDefaultMeaning
rawunknownrequiredThe style as it arrived.

Returns — a LoaderStyle keeping only known kinds, sizes and speeds, a colour that is "accent" or #RRGGBB (uppercased), a boolean skeleton, and words trimmed to LOADER_TEXT_MAX; {} for anything that is not an object. Example value: { kind: "ring", color: "#3740B8" }.

Errors — none.

normaliseLoaderStyle({ kind: "Ring", color: "#3740b8", size: "huge" }); // { kind: "ring", color: "#3740B8" }

resolveLoader

Use it to turn a loader style into the numbers to draw.

function resolveLoader(style: LoaderStyle | null | undefined): LoaderLook
ParameterTypeDefaultMeaning
styleLoaderStyle | null | undefinedrequiredThe style (checked again here).

Returns — a LoaderLook with every default filled. Example value: { kind: "dots", color: null, px: 20, periodMs: 1100, text: { connecting: "Connecting…", reconnecting: "Reconnecting…", thinking: "Thinking…", component: "Loading…" }, skeleton: false }.

Errors — none.

const { appearance } = useGraineAgent();
const look = resolveLoader(appearance?.loader);

MIN_LOADER_MS

Read it as how long a loader, once shown, stays — so a quick answer reads as "waiting, then the answer" rather than a flicker.

const MIN_LOADER_MS = 450;

What to expect — 450 milliseconds; a component that arrives sooner waits that long behind its loader.

FrictionConfig

Read it to tune when the agent is told the customer looks stuck — idle on a step, a field rejecting them, going back and forth, tapping in frustration, pressing on with fields left, an error on screen.

What to expect — Layered: the built-in defaults (DEFAULT_FRICTION), then the dashboard's rules for the agent, then your app's (the provider's friction, or configureFriction). Each layer changes only what it names; false turns a rule off; unknown keys and bad numbers are ignored. A signal is told to the agent (which decides whether to speak) and to your app as the friction_detected event.

FieldTypeDefaultMeaning
enabledbooleantrueTurns friction detection on or off everywhere.
cooldownMsnumber60000The least time between two signals on the same screen.
nudgeFrictionNudgesee FrictionNudgeHow often the agent may volunteer, and whether a signal should open your launcher.
screensRecord<string, FrictionScreenRules>{}Rules for single screens, keyed by the screen id you report.
ignore, idle, repeatedErrors, backAndForth, rageTaps, incompleteSubmit, screenError, message—as FrictionScreenRulesThe rules themselves, for every screen.
<GraineProvider friction={{ screens: { terms: { ignore: true }, otp: { idle: { afterMs: 20000 } } } }} … />

FrictionScreenRules

Read it to change the rules for one screen (or, at the top level of FrictionConfig, for all of them); every field is optional.

FieldTypeDefaultMeaning
ignorebooleanfalseNo friction detection on this screen at all.
idle{ afterMs: number } | false{ afterMs: 45000 }Untouched on a step this long → stalled_on_step. Fires once per visit to the screen.
repeatedErrors{ count: number; windowMs: number } | false{ count: 2, windowMs: 60000 }The same field showing an error this many times in the window → repeated_field_error.
backAndForth{ visits: number; windowMs: number } | false{ visits: 3, windowMs: 45000 }Arriving on the same screen this many times in the window → looping_between_screens.
rageTaps{ taps: number; windowMs: number } | false{ taps: 4, windowMs: 1500 }The same tap this many times in the window → rage_taps.
incompleteSubmit{ taps: string[] } | falsesubmit, next, continue, save, proceed, apply, confirm, done, pay, verifyA tap whose name contains one of these words (any case), while fields are still empty or invalid → incomplete_submit. Set false where pressing on with blanks is fine.
screenErrorbooleantrueAn error banner on the screen (ScreenContext.error) is itself a signal → screen_error.
messagestringthe built-in sentenceWhat the agent is told instead, with {title}, {screen}, {field}, {error}, {count}, {seconds} and {tap} filled in.

FrictionNudge

Read it to limit how often the agent volunteers after a signal.

FieldTypeDefaultMeaning
cooldownSecondsnumber20Seconds the agent waits between two unprompted offers of help.
maxPerSessionnumber3The most unprompted offers in one conversation.
openLauncherbooleanfalseFor a launcher you draw yourself: whether a signal should open it when no conversation is running. The SDK only carries the flag; your app decides what opening means.

FrictionSignal

Read it as one moment the customer looked stuck — what the agent is told, and what your app hears.

FieldTypeDefaultMeaning
nameFrictionKindrequiredWhich rule fired.
screenstringrequiredThe screen id it fired on.
detailstringrequiredThe sentence the agent is told — the built-in one, or your message filled in.
dataRecord<string, unknown>requiredThe numbers behind it — count, seconds, field, error, tap.
cooldown_secondsnumberrequiredHow long the agent waits before volunteering again (nudge.cooldownSeconds).
max_per_sessionnumberrequiredThe most unprompted offers in the conversation (nudge.maxPerSession).

FrictionKind

Read it as the names of the friction signals.

type FrictionKind = "stalled_on_step" | "repeated_field_error" | "looping_between_screens" | "rage_taps" | "incomplete_submit" | "screen_error";
ValueMeaning
"stalled_on_step"Untouched on a step for idle.afterMs.
"repeated_field_error"A field rejected the customer repeatedErrors.count times.
"looping_between_screens"The customer keeps coming back to a screen without getting past it.
"rage_taps"The same control (or the screen) tapped again and again in a moment.
"incomplete_submit""Next" or "Submit" pressed with fields still to fill.
"screen_error"The screen is showing an error banner.

DEFAULT_FRICTION

Read it as the built-in friction rules every layer starts from.

const DEFAULT_FRICTION: Readonly<Required<Omit<FrictionConfig, "message" | "ignore">>>;
KeyValueMeaning
enabledtrueFriction detection is on.
cooldownMs60000A minute between two signals on one screen.
idle{ afterMs: 45000 }45 seconds untouched.
repeatedErrors{ count: 2, windowMs: 60000 }Twice in a minute.
backAndForth{ visits: 3, windowMs: 45000 }Three arrivals in 45 seconds.
rageTaps{ taps: 4, windowMs: 1500 }Four taps in 1.5 seconds.
incompleteSubmit{ taps: ["submit", "next", "continue", "save", "proceed", "apply", "confirm", "done", "pay", "verify"] }The words that mean "move me on".
screenErrortrueAn error banner counts.
nudge{ cooldownSeconds: 20, maxPerSession: 3, openLauncher: false }The agent's pacing.
screens{}No per-screen rules.

resolveFriction

Use it to combine friction layers the way the SDK does.

function resolveFriction(...layers: Array<FrictionConfig | null | undefined>): Required<FrictionConfig>
ParameterTypeDefaultMeaning
layersArray<FrictionConfig | null | undefined>noneThe layers, lowest first — the dashboard's, then the app's. Empty layers are skipped.

Returns — the defaults with each layer applied in turn, every field filled; unknown keys are dropped and numbers that are not positive fall back. Example value: { enabled: true, cooldownMs: 60000, idle: { afterMs: 20000 }, … }.

Errors — none.

const rules = resolveFriction(client.config?.friction, { idle: { afterMs: 20000 } });   // the dashboard's, then yours

rulesFor

Use it to see the rules that apply on one screen: the top level with that screen's own rules over it.

function rulesFor(config: Required<FrictionConfig>, screenId: string | null | undefined): Required<FrictionScreenRules>
ParameterTypeDefaultMeaning
configRequired<FrictionConfig>requiredResolved rules — client.friction.
screenIdstring | null | undefinedrequiredThe screen's id.

Returns — every rule for that screen. Example value: { ignore: false, idle: { afterMs: 20000 }, repeatedErrors: { count: 2, windowMs: 60000 }, … }.

Errors — none.

rulesFor(client.friction, "otp").idle;

FrictionDetector

Use it only to run friction detection on your own timers; the client runs one for you.

class FrictionDetector {
  constructor(config?: FrictionConfig | Required<FrictionConfig>)
  configure(config: Required<FrictionConfig>): void
  get settings(): Required<FrictionConfig>
  onScreen(ctx: ScreenContext | null, now: number): FrictionSignal[]
  onInteraction(now: number): void
  onTrack(name: string, now: number): FrictionSignal[]
  idleDueIn(now: number): number | null
  onIdle(now: number): FrictionSignal | null
}
MemberTypeMeaning
constructor(config?)Starts with resolveFriction(config).
configure(config: Required<FrictionConfig>) => voidReplaces the rules (pass resolved rules).
settingsRequired<FrictionConfig>The rules in force.
onScreen(ctx, now) => FrictionSignal[]Report a screen at time now (milliseconds); returns what can be known at once — a repeated field error, an error banner, a loop between screens. A new screen id is a new visit; the same id is an update.
onInteraction(now) => voidThe customer did something: the idle clock restarts.
onTrack(name, now) => FrictionSignal[]Report a tracked event; only taps (tap:…) are read — for rage taps and pressing on with fields left.
idleDueIn(now) => number | nullMilliseconds until the idle rule should fire, or null when it should not.
onIdle(now) => FrictionSignal | nullThe idle timer went off: the stalled_on_step signal, at most once per visit.

Errors — none. Every signal respects cooldownMs per screen.

const detector = new FrictionDetector(client.friction);
const signals = detector.onScreen({ screen: "checkout", title: "Checkout", error: "Your card was declined" }, Date.now());

GraineInAppClient.configureFriction

Call it to replace your app's layer of friction rules at any time — after a remote setting changes, say.

configureFriction(override: FrictionConfig | null | undefined): void
ParameterTypeDefaultMeaning
overrideFrictionConfig | null | undefinedrequiredYour app's rules; null removes your layer, leaving the defaults and the dashboard's.

Returns — nothing. The rules apply at once and the idle timer restarts.

Errors — none.

client.configureFriction({ screens: { checkout: { idle: { afterMs: 30000 } } } });

GraineInAppClient.friction

Read it to see the friction rules in force, every layer applied.

get friction(): Required<FrictionConfig>

Returns — the resolved rules. Example value: { enabled: true, cooldownMs: 60000, idle: { afterMs: 45000 }, … }.

Errors — none.

console.log(client.friction.idle);

addMaskRule

Call it once at startup to mask an identifier only your business knows — an account number format — before anything leaves the device.

function addMaskRule(pattern: RegExp, token = "[REDACTED]"): void
ParameterTypeDefaultMeaning
patternRegExprequiredWhat to mask. It is made global, so every match is replaced.
tokenstring"[REDACTED]"What replaces each match.

Returns — nothing. The rule applies everywhere the SDK masks — screens, fields, traits, variables, events — after the built-in rules, and in your own maskString and maskDeep calls.

Errors — none.

addMaskRule(/\bACC-\d{8}\b/, "[ACCOUNT]");

maskDeep

Use it to mask a whole payload as the SDK does before sending it: every string inside it, at any depth.

function maskDeep<T>(value: T): T
ParameterTypeDefaultMeaning
valueTrequiredAny value — a string, an object, a list.

Returns — a copy with every string masked by the built-in rules and then yours; numbers and booleans are left as they are (so amounts stay numbers). A Date becomes its ISO string, as JSON sends it (null for an invalid date), masked like any string. Pass plain data otherwise: another class instance comes back as a plain object of its own fields. Example value: { email: "[EMAIL]", amount: 1200 }.

Errors — none.

maskDeep({ email: "asha@example.com", amount: 1200 }); // { email: "[EMAIL]", amount: 1200 }

maskString

Use it to mask one string as the SDK does: the built-in rules, then yours.

function maskString(input: string): string
ParameterTypeDefaultMeaning
inputstringrequiredThe text.

Returns — the text with email addresses → [EMAIL], card numbers (13–19 digits) → [CARD], mobile numbers → [PHONE], and the built-in national identifier formats → [PAN] and [AADHAAR] — then every rule added with addMaskRule, the same pass maskDeep gives each string. Example value: "Write to [EMAIL]".

Errors — none.

maskString("Write to asha@example.com"); // "Write to [EMAIL]"

LauncherVisibility

Read it to say where and when the launcher may appear, per flow — the provider's visibility prop.

What to expect — A screen may show the launcher when any rule names it: includeScreens or a group's screens. With no rules at all, every screen does.

FieldTypeDefaultMeaning
defaultDelayMsnumberthe provider's launcherDelayMsThe wait before the launcher appears, for screens whose group sets none.
defaultInsetLauncherInset{ right: 16, bottom: 20 }Where the launcher sits, for screens whose group sets none.
groupsLauncherGroup[]noneFlows of screens, each with its own rules — see LauncherGroup.
<GraineProvider visibility={{ groups: [{ id: "checkout", screens: ["Cart", "Address", "Payment"], continuity: "continuous", delayMs: 3000, delayPolicy: "oncePerGroupEntry" }] }} … />

LauncherGroup

Read it to treat several screens as one flow for the launcher.

FieldTypeDefaultMeaning
idstringrequiredThe group's id, unique — the delay policies remember it.
screensstring[]requiredRoute names in the flow, matched exactly (case matters).
continuityLauncherContinuityrequiredWhether the launcher stays put while the customer moves inside the flow.
insetLauncherInsetdefaultInsetWhere the launcher sits on these screens.
delayMsnumberdefaultDelayMsThe wait before it appears.
delayPolicyLauncherDelayPolicy"perScreen"When that wait is spent.

LauncherInset

Read it as where the launcher sits: its distance from each edge of the screen, in points.

FieldTypeDefaultMeaning
topnumbernoneDistance from the top edge.
rightnumbernoneDistance from the right edge (from the left edge for a left-hand launcher).
bottomnumbernoneDistance from the bottom edge — clear your tab bar with it.
leftnumbernoneDistance from the left edge.

LauncherContinuity

Read it as whether the launcher stays put while the customer moves inside one flow.

type LauncherContinuity = "continuous" | "perScreen";
ValueMeaning
"continuous"The flow is one place: moving between its screens keeps the launcher where it is, with no new delay.
"perScreen"Each screen decides afresh.

LauncherDelayPolicy

Read it as when a group's delay is spent.

type LauncherDelayPolicy = "perScreen" | "oncePerGroupEntry" | "oncePerAppSession";
ValueMeaning
"perScreen"Every time the customer lands on a screen.
"oncePerGroupEntry"Once on entering the flow; free inside it.
"oncePerAppSession"Once per app run — an offer, not a follow.

LauncherVisibilityTracker

Use it only to apply the launcher visibility rules to your own navigation; the provider runs one for you with navigationRef.

class LauncherVisibilityTracker {
  constructor(includeScreens: string[] | undefined, visibility: LauncherVisibility | undefined, defaultDelayMs = 0)
  reset(): void
  onRoute(route: string): VisibilityDecision
}
MemberTypeMeaning
constructor(includeScreens, visibility, defaultDelayMs = 0)The provider's includeScreens, visibility and launcherDelayMs.
reset() => voidForget the delays already spent — on a new call or a new app session.
onRoute(route: string) => VisibilityDecisionCall it on every route change, in order: returns whether the launcher may show on this screen, after how long and where.

Errors — none.

const tracker = new LauncherVisibilityTracker(["Home", "Orders"], undefined, 1500);
const decision = tracker.onRoute("Home");

VisibilityDecision

Read it as the launcher's verdict for one screen.

FieldTypeDefaultMeaning
eligiblebooleanrequiredWhether the launcher may be on this screen at all.
delayMsnumberrequiredHow long to wait before showing it, with the delay policy already applied.
insetLauncherInsetrequiredWhere it sits on this screen.
groupIdstring | nullrequiredThe group that decided — for debugging a rule that surprises you.
continuousbooleanrequiredtrue when the launcher should stay where it is rather than appear again.

activeRouteName

Use it to read the screen the customer is actually on from a React Navigation state — the deepest active route, not a navigator's name.

function activeRouteName(state: any): string | null
ParameterTypeDefaultMeaning
stateanyrequiredA navigation state — navigationRef.getRootState().

Returns — the name of the deepest focused route, or null for a state it cannot read. Example value: "OrderDetail".

Errors — none.

const screen = activeRouteName(navigationRef.getRootState());

Calls the user places

Use @graineai/inapp-react-native-calls for calls that behave like a phone call — CallKit on iOS, a full-screen call on Android, going on with the screen off — when the customer calls the agent from your app. Version 0.3.0 runs the call engine of Graine's iOS and Android SDKs 0.8.0.

GraineCalls

Use it as the calls package's one entry point: configure the engine, place and control calls, hand the call what your app tells the agent, and draw your own call screen from its state.

const GraineCalls: {
  readonly isAvailable: boolean;
  configure(options: GraineCallsOptions): Promise<void>;
  configure(options: GraineCallsAgentsOptions): Promise<void>;
  configure(name: string, options: GraineCallsAgentOptions | GraineCallsOptions): Promise<void>;
  readonly agents: string[];
  enableVoIPPushes(onToken: (token: string | null) => void): () => void;
  handlePush(data: Record<string, string>): Promise<boolean>;
  registerPushToken(push: { token: string; kind?: "fcm" | "voip"; environment?: "production" | "sandbox" }): Promise<boolean>;
  installId(): Promise<string | null>;
  startCall(options?: GraineStartCallOptions): Promise<string | null>;
  answer(): Promise<void>;
  decline(): Promise<void>;
  end(): Promise<void>;
  setMuted(muted: boolean): void;
  setSpeaker(on: boolean): void;
  attach(client: GraineAttachableClient, options?: { agent?: string }): () => void;
  onOutcome(fn: (o: GraineCallOutcome) => void): () => void;
  onIncomingCall(fn: (c: GraineIncomingCall) => void): () => void;
  getSnapshot(): GraineCallSnapshot;
  subscribe(fn: () => void): () => void;
  callPermissions(): Promise<GraineCallPermissions>;
  requestCallPermissions(): Promise<GraineCallPermissions>;
  openCallSettings(item: GraineCallPermission): Promise<boolean>;
  callSetupProblems(): Promise<GraineCallSetupProblem[]>;
};

What to expect — One engine for the whole app, one call at a time on the device. Configure it once at launch, before a push can arrive; its options are kept on the device, so a call that wakes a closed app rings and connects before any JavaScript runs. A method that needs the native code throws Error("@graineai/inapp-react-native-calls is not linked: rebuild the app (pod install on iOS) — it carries native code, so an over-the-air update cannot add it.") in a build without it — check GraineCalls.isAvailable first.

Placing calls: configure, startCall, end, setMuted, setSpeaker, attach, getSnapshot, subscribe. Calls Graine rings: enableVoIPPushes, handlePush, registerPushToken, installId, answer, decline, onIncomingCall, onOutcome. Permissions and the setup check: callPermissions, requestCallPermissions, openCallSettings, callSetupProblems. Several agents: agents. Going live: isAvailable.

GraineCalls.configure

Call it once at launch — in index.js, before a push can arrive — to give the call engine your agent's key and the name its calls show; call it again by name to add more agents.

configure(options: GraineCallsOptions): Promise<void>
configure(options: GraineCallsAgentsOptions): Promise<void>
configure(name: string, options: GraineCallsAgentOptions | GraineCallsOptions): Promise<void>
ParameterTypeDefaultMeaning
optionsGraineCallsOptionsrequiredThe default agent and how calls behave — see GraineCallsOptions. With agents, more agents by name; without a top-level publishableKey (GraineCallsAgentsOptions) the first agent listed is the default.
namestring—One agent by your name for it: its publishableKey (and its own baseUrl). The first agent configured becomes the default when there is none yet; "default", or the default agent's own name, updates the default agent.

Returns — Promise<void>, once the engine has the options; the call state is read then, so getSnapshot() is current.

Errors — rejects, and keeps the last configuration (every agent is checked before anything changes):

  • Error("publishableKey is required") or Error("displayName is required") for the default agent;
  • Error('publishableKey is required for the agent "sales"') for a named agent without a key;
  • Error("An agent's name must be a non-empty string.");
  • Error('"default" is the agent with the top-level publishableKey; name the others') when agents lists "default" beside a top-level key;
  • code invalid_options, with a message naming the option, when the engine refuses a value — for example callTransport must be "auto", "websocket" or "webrtc";
  • the not-linked error above.
// index.js
GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Acme" });
 
// several agents
await GraineCalls.configure({
  displayName: "Acme",
  agents: { support: { publishableKey: "pk_live_support…" }, sales: { publishableKey: "pk_live_sales…" } },
});
await GraineCalls.configure("billing", { publishableKey: "pk_live_billing…" });

GraineCallsOptions

Read it to configure the call engine: the default agent's key, the name calls show, and how calls ring and look.

FieldTypeDefaultMeaning
publishableKeystringrequiredThe default agent's publishable key (Agent → Embed & Widgets). Public by design.
baseUrlstring"https://www.graine.ai"Where Graine is.
displayNamestringrequiredThe name on the ring and the call screen — "Acme assistant" — for agents whose dashboard settings name none.
ringtoneSoundstringthe phone's ringtoneiOS: a sound file of 30 seconds or less in your app bundle. Android: a raw resource name.
ringTimeoutMsnumber30000How long a ring lasts before it counts as missed, in milliseconds.
appVersionstringnoneYour app's version, so each call's record says which build placed it.
callTransportGraineCallTransport"auto"What carries the calls' audio, in both directions — calls the customer places and calls Graine rings: "auto" (WebRTC in a build with WebRTC calls, else the WebSocket), "websocket" or "webrtc". The agent's In-app call audio setting can still move every call to the WebSocket. See GraineCallTransport.
callScreen"sdk" | "app""sdk"Who draws the call while your app is open: "sdk" — the SDK's call screen (a full-screen activity on Android, a full-screen view over your app on iOS); "app" — your own screen, drawn from useGraineCall(). On a locked iPhone, the system's call screen appears whatever this is set to.
debugbooleanfalseThe native call engine's debug log in Xcode's console or logcat — how a call connected and what its audio did. Turn it on in debug builds only; it is read at launch, so a change applies at the next launch.
android{ smallIcon?: string }noneAndroid only: smallIcon, the drawable resource name for the call notifications.
ios{ iconTemplate?: string; includesCallsInRecents?: boolean }noneiOS only: iconTemplate, a template image in your bundle shown on the system call screen; includesCallsInRecents (false), whether calls appear in the Phone app's Recents.
agentsRecord<string, GraineCallsAgentOptions>noneMore agents, by your name for each — see GraineCallsAgentOptions. When Graine rings a call, it arrives as the agent that booked it.

GraineCallTransport

Read it to choose what carries the calls' audio — GraineCallsOptions.callTransport — for every call: the ones the customer places and the ones Graine rings.

type GraineCallTransport = "auto" | "websocket" | "webrtc";

What to expect — It matters only in a build with WebRTC calls; without them every call runs on the WebSocket, whatever it says. The agent's In-app call audio setting decides last: Standard keeps every call on the WebSocket, whatever the app chose. The words are case-sensitive: any other makes configure() reject with code invalid_options. Which one carried a call: transport and transportFallbackReason on GraineCallSnapshot.

ValueMeaning
"auto"The default. WebRTC when the build includes it and the dashboard allows it; otherwise the WebSocket.
"websocket"Always the WebSocket.
"webrtc"WebRTC wherever it can carry the call; the WebSocket only when the build does not include it or WebRTC cannot set this call up. On Android, a build without it says so: transportFallbackReason "unavailable".
await GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Acme", callTransport: "websocket" });   // this build's calls stay on the WebSocket

GraineCalls.startCall

Call it from your "Call us" button: the customer calls the agent — the default one, or one you name — on a real call.

startCall(options: GraineStartCallOptions = {}): Promise<string | null>
ParameterTypeDefaultMeaning
optionsGraineStartCallOptions{}Why the customer is calling, prompt variables for this call, and which agent — see GraineStartCallOptions.

Returns — Promise<string | null>: the call's id — every outcome carries it — or null when no call was placed: one is already live (one at a time), or, on Android, the customer refused the microphone (then getSnapshot().error reads "The microphone permission was denied."). Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — rejects with Error('No agent named "sales" is configured. Call GraineCalls.configure("sales", { publishableKey, … }) first.') for an agent you have not configured, and when a named agent meets an engine from before several agents (rebuild the app); throws the not-linked error in a build without the package.

What to expect — The package asks for the microphone first. The agent hears who_called = "customer" and call_reason = your reason as prompt variables, with your variables and the attached client's. Since 0.3.0 a call starts at once on the configuration already kept on the device and refreshes it behind the call. In a build with WebRTC calls it is prepared at the tap and carries its audio over WebRTC, unless callTransport or the dashboard says otherwise — transport on the snapshot says which. Then the call reports through the snapshot and onOutcome: ended (with who ended it) once connected, or failed with why it never connected — see GraineCallOutcome for the 0.3.0 failure lines.

const id = await GraineCalls.startCall({ reason: "Talk through my order", agent: "support" });
if (id === null) showToast(GraineCalls.getSnapshot().error ?? "A call is already in progress");

GraineStartCallOptions

Read it to place a call: why, with what variables, and with which agent.

FieldTypeDefaultMeaning
reasonstringnoneWhy the customer is calling — the agent hears it as the call_reason prompt variable.
variablesRecord<string, unknown>nonePrompt variables for this call only.
agentstringthe default agentThe configured agent to call, by your name for it.

GraineCalls.end

Call it from your call screen's hang-up button — while a call rings, it declines it.

end(): Promise<void>

Parameters — none.

Returns — Promise<void>. A connected call reports ended with reason: "local"; a ring reports declined. On Android 0.8.0's engine, hanging up lets go of the microphone at once.

Errors — throws the not-linked error in a build without the package.

await GraineCalls.end();

GraineCalls.setMuted

Call it from your call screen's mute button: the customer's voice stops reaching the agent and the call goes on.

setMuted(muted: boolean): void
ParameterTypeDefaultMeaning
mutedbooleanrequiredtrue to mute the customer; false to unmute.

Returns — nothing; useGraineCall().muted follows.

Errors — throws the not-linked error in a build without the package.

const call = useGraineCall();
GraineCalls.setMuted(!call.muted);

GraineCalls.setSpeaker

Call it from your call screen's speaker button to move the call between the loudspeaker and the earpiece (or a headset).

setSpeaker(on: boolean): void
ParameterTypeDefaultMeaning
onbooleanrequiredtrue for the loudspeaker; false for the earpiece, or a connected headset. Calls start on the earpiece, like a phone call.

Returns — nothing; useGraineCall().speakerOn follows.

Errors — throws the not-linked error in a build without the package.

const call = useGraineCall();
GraineCalls.setSpeaker(!call.speakerOn);

GraineCalls.attach

Call it once, inside the provider, so a call knows what your app tells the agent — who the customer is, your variables, the screen, recent events — and the agent's actions run through your handlers during a call.

attach(client: GraineAttachableClient, options: { agent?: string } = {}): () => void
ParameterTypeDefaultMeaning
clientGraineAttachableClientrequiredYour agent's client — useGraineClient().
options.agentstringmatched by the client's keyWhich configured agent this client is; needed only for a client without opts.publishableKey.

Returns — a function that detaches the client.

Errors — throws the not-linked error in a build without the package.

What to expect — Kept in step as things change: the customer's traits (handed over raw — the engine masks them and takes out the identifying value), the memory choice, your variables, the screen and the full context (at most every 400 ms), and the actions you registered (the engine asks your client to run them, and answers refused for one this build lacks). While a call rings, connects or is connected, the client is told, so its voice bar ends its conversation and refuses to start one ("A call is already in progress."). With several agents, attach each agent's client: the one for the agent on the call feeds the engine and runs that agent's actions, else the default agent's.

function CallsBridge() {
  const client = useGraineClient();
  useEffect(() => GraineCalls.attach(client), [client]);
  return null;
}

GraineAttachableClient

Read it as what attach() needs from a client — GraineInAppClient from @graineai/inapp-react-native 0.40.1 or later has all of it.

MemberTypeDefaultMeaning
on(event: string, fn: (payload: any) => void) => () => voidrequiredHow the package hears the client's variables, screen and identity changes, to pass them to a live call.
getIdentity() => Record<string, string>requiredThe customer's masked traits — used when getRawTraits is missing.
getRawTraits() => Record<string, unknown>optionalThe customer's traits as given, so the engine can take out the identifying value itself (0.41+).
memoryEnabledbooleanoptionalThe customer's memory choice, so a call is remembered as the chat is (0.41+).
buildContextFrame() => object | nulloptionalThe full context — screen, customer, recent events and taps — sent to the call (0.41+); without it the engine builds one from getScreen().
getVariables() => Record<string, string>requiredYour prompt variables, for the call.
getScreen() => object | nullrequiredThe screen the customer is on, for the call.
executeAction(name: string, args?: Record<string, unknown>) => Promise<GraineActionResult>requiredRuns one of your actions when the agent asks for it during a call.
getAvailableActions() => string[]optionalThe actions you registered, so the engine offers them.
setNativeCallLive(live: boolean) => voidoptionalLets the voice bar step aside while a call is live (0.40.2+); without it the bar beside a call is yours to manage.
opts{ readonly publishableKey?: string }optionalThe client's options — its key matches it to the configured agent with the same key.

GraineActionResult

Read it as the answer an action gives during a call — the same three outcomes as AppActionResult.

FieldTypeDefaultMeaning
status"ok" | "refused" | "error"requiredok — it happened; refused — declined (or not in this build); error — it failed.
messagestringoptionalA sentence for the agent about what happened.
dataRecord<string, unknown>optionalStructured results for the agent.

GraineInAppClient.nativeCallLive

Read it to know whether a call from the calls package is ringing, connecting or connected — while it is, the voice bar does not start.

get nativeCallLive(): boolean

Returns — true from the moment such a call rings or is placed until it is over (told by GraineCalls.attach()). Example value: false.

Errors — none.

if (client.nativeCallLive) hideVoiceButton();

GraineInAppClient.setNativeCallLive

GraineCalls.attach() calls it as a call goes live and ends; your app does not need it.

setNativeCallLive(live: boolean): void
ParameterTypeDefaultMeaning
livebooleanrequiredtrue while a call rings, connects or is connected; false when it is over.

Returns — nothing. Every agent's client on the device is told; each announces native_call ({ live }), and its voice launcher ends its conversation and refuses to start while it is true.

Errors — none.

client.setNativeCallLive(true);

GraineCalls.getSnapshot

Read it for the call as it stands right now, outside React.

getSnapshot(): GraineCallSnapshot

Parameters — none.

Returns — the current GraineCallSnapshot; idle before anything has happened. Example value: { state: "active", callId: "call_6f1c…", direction: "outgoing", muted: false, speakerOn: false, agentSpeaking: true, connectedAt: 1760500000000, error: null, agent: "default", transport: "webrtc", transportFallbackReason: null, … }.

Errors — none.

const { state } = GraineCalls.getSnapshot();

GraineCalls.subscribe

Call it to be told every time the call changes, outside React.

subscribe(fn: () => void): () => void
ParameterTypeDefaultMeaning
fn() => voidrequiredCalled on every change; read the new state with getSnapshot().

Returns — a function that unsubscribes.

Errors — throws the not-linked error in a build without the package (it starts listening to the engine).

const off = GraineCalls.subscribe(() => updateCallScreen(GraineCalls.getSnapshot()));

useGraineCall

Call it in your own call screen to draw the call — it re-renders as the call changes.

function useGraineCall(): GraineCallSnapshot

Parameters — none.

Returns — the current GraineCallSnapshot.

Errors — throws the not-linked error in a build without the package.

const call = useGraineCall();
if (call.state === "idle") return null;
return <CallScreen name={call.callerName} speaking={call.agentSpeaking} onHangUp={() => GraineCalls.end()} />;

GraineCallSnapshot

Read it to draw a call: its state, who and why, the controls' state, the live caption, what carries its audio and, when it failed, why.

interface GraineCallSnapshot { state: GraineCallState; callId: string | null; …; agent: string | null; transport: "webrtc" | "websocket" | null; transportFallbackReason: string | null }

What to expect — idle → ringing (a call coming in) or connecting (a call placed, or answered) → active → ended, then idle again once reported. One call at a time: a ring during a live call is reported missed without disturbing it. transport and transportFallbackReason describe calls in both directions — placed or answered — and are final once the call is active.

FieldTypeDefaultMeaning
stateGraineCallStaterequiredWhere the call is — see GraineCallState.
callIdstring | nullrequiredThe call's id: your server's for a call it rang, a generated one for a call the customer placed; null while idle.
direction"incoming" | "outgoing" | nullrequiredWhether the call came in (a ring) or was placed; null while idle.
reasonstring | nullrequiredWhy the call is happening — the reason it was placed or rung with.
callerNamestring | nullrequiredThe name the call shows.
mutedbooleanrequiredWhether the customer is muted.
speakerOnbooleanrequiredWhether the loudspeaker is on.
agentSpeakingbooleanrequiredWhether the agent's voice is playing now — for a speaking animation.
captionGraineCaption | nullrequiredThe line being said now, by either side.
connectedAtnumber | nullrequiredWhen the conversation opened, in milliseconds since 1970 — for a call timer.
errorstring | nullrequiredWhy the last call could not connect, when it could not — the failed outcome's line (see GraineCallOutcome), or, on Android, "The microphone permission was denied." Cleared when the next call starts; a call that connected and then ended leaves none.
agentstring | nullrequiredWhich configured agent the call is with — "default" or your name for it; null while idle, and from an engine that predates several agents.
transport"webrtc" | "websocket" | nullrequiredWhat carries this call's audio: "webrtc" or "websocket", once decided — at the tap, or when the ring arrives; a call that falls back reads "websocket" before any audio flows. null with no call, and from an engine that predates WebRTC. See WebRTC calls.
transportFallbackReasonstring | nullrequiredWhy a call that tried WebRTC runs on the WebSocket — one of the values below; null when it did not fall back, and before a transport is decided. Read it once the call is active.

Why a call fell back to the WebSocket — the values of transportFallbackReason:

ValueMeaning
"mint_failed"The call's WebRTC session could not be set up — refused, rate-limited, or unreachable.
"register_failed"Graine's media service did not accept the connection, or never answered it.
"no_response"The call got no reply at all.
"no_answer"Replies came; the answer did not.
"ice_timeout"Answered, but the audio path never connected — a network that blocks WebRTC.
"error"Anything else before the audio flowed.
"dashboard"Android only: the agent's In-app call audio setting is Standard. iOS leaves the reason null here.
"unavailable"Android only: callTransport: "webrtc" in a build that cannot run WebRTC calls — built without them, or their library could not load. iOS leaves the reason null here.
const call = useGraineCall();
if (call.state === "active" && call.transportFallbackReason) console.log("on the WebSocket:", call.transportFallbackReason);

GraineCallState

Read it as where a call is.

type GraineCallState = "idle" | "ringing" | "connecting" | "active" | "ended";
ValueMeaning
"idle"No call.
"ringing"A call is coming in and ringing.
"connecting"A call was placed, or answered, and is being connected.
"active"The conversation is open — the agent can hear the customer.
"ended"The call just ended; idle follows once the outcome is reported.

GraineCaption

Read it to draw the live caption of a call.

FieldTypeDefaultMeaning
role"agent" | "customer"requiredWho is speaking.
textstringrequiredWhat has been said so far in this line.
livebooleanrequiredtrue while the line is still being said.

GraineCallOutcome

Read it to report what became of each call to your server — onOutcome hands it over once per change, with the call's id.

type GraineCallOutcome = (
  | { callId: string; outcome: "ringing" } | { callId: string; outcome: "answered" }
  | { callId: string; outcome: "declined" } | { callId: string; outcome: "missed" }
  | { callId: string; outcome: "ended"; reason: "local" | "remote" | "failed" }
  | { callId: string; outcome: "failed"; error: string }
) & { durationSeconds?: number | null; hosted?: boolean; agent?: string | null };

What to expect — Every outcome carries callId. Do not rely on its order relative to the snapshot's state changes. A value newer than this package arrives as its word.

outcomeFieldsMeaning
ringingcallId, hostedA call Graine rang is ringing on this device (hosted: true; Graine is told too).
answeredcallIdThe customer answered a ring; ended or failed follows.
declinedcallIdThe customer declined a ring.
missedcallIdA ring nobody answered in time — or one that came while another call was live.
endedcallId, reason, durationSecondsA connected call ended: reason "local" (this app hung up), "remote" (the agent ended it) or "failed" (the connection broke mid-call); durationSeconds when the engine measured it.
failedcallId, errorThe call never connected; error says why, in a sentence.

Every outcome may also carry hosted (true for a call Graine rang — Graine has its outcome already) and agent (the configured agent the call was with).

The failure lines (0.3.0) — what error reads, and when:

Platformerror, or what you receiveWhen
iOS"The call could not connect within 20 seconds."One deadline now covers everything before the conversation opens — the agent's settings, the call's ticket and the connection — so a call never sits on "Connecting" for most of a minute.
iOS"The conversation did not open within 15 seconds."The connection's own open limit: it starts once the call's ticket is in hand, so it can end the call before the 20 seconds are up.
iOS"The call's audio could not start."The conversation opened but the system never handed the call its audio: the engine waits 3 seconds, starts the audio itself once, and fails the call 3 seconds later if it still does not run — instead of a call nobody can hear.
iOS"The microphone permission was not granted."The customer refused the microphone for a call they placed or answered.
Android"The call could not connect within 15 seconds."The connection did not open in time (unchanged).
Androidoutcome: "ended", reason: "failed"New: a connected call where nothing at all arrived for 50 seconds (two keep-alive periods) — a network that died silently — ends instead of staying open and silent. The engine words it "Nothing arrived for 50 seconds: the connection was lost."; the outcome here carries the reason only.
Eitheranother sentence naming what failedFor example "The agent's configuration could not be loaded." on iOS; "The call was refused (close …)." or "The call could not connect: " and the detail on Android.
GraineCalls.onOutcome((o) => {
  const details = o.outcome === "failed" ? { error: o.error } : o.outcome === "ended" ? { reason: o.reason, seconds: o.durationSeconds } : {};
  myServer.reportCall(o.callId, o.outcome, details);
});

EVENTS

Read it only if you listen to the native module yourself — the names its events carry; onOutcome, onIncomingCall, subscribe and enableVoIPPushes listen for you.

const EVENTS = { state: "GraineCallState", outcome: "GraineCallOutcome", incoming: "GraineIncomingCall", voipToken: "GraineVoIPToken", action: "GraineActionRequest" } as const;
KeyValueMeaning
state"GraineCallState"A new call snapshot.
outcome"GraineCallOutcome"A call's outcome.
incoming"GraineIncomingCall"A ring started.
voipToken"GraineVoIPToken"iOS: the VoIP push token, or null when it was invalidated.
action"GraineActionRequest"The agent asked for one of your actions during a call.

Calls Graine rings

Use these so a call can ring the customer's phone — on the lock screen, with the app closed — when Graine rings them for a call they or the agent booked, or when your own server rings them.

What to expect — Graine rings an identified customer's devices for booked calls when your workspace has In-app calling set up; on Android, with Firebase in the app (Expo: android.googleServicesFile and the package's plugin; bare: graineFirebaseMessaging=true in android/gradle.properties), the package registers for pushes itself — an app whose own messaging service receives the pushes (@react-native-firebase/messaging) forwards them with handlePush and registerPushToken — and on iOS the VoIP token is registered for you. Your own server can ring too, with one push of string values: type: "agent_call", call_id (your id — every outcome carries it back) and optionally reason, caller_name and variables (a JSON object, as a string) — on iOS a VoIP push to APNs (apns-push-type: voip, topic <bundle id>.voip) to the token from enableVoIPPushes; on Android a data-only FCM message at high priority. The push carries nothing personal. A call Graine rang tells the agent who_called = "scheduled" for a booked time (else "agent"). A call that rings carries its audio exactly as a call the customer places: over WebRTC in a build with WebRTC calls — prepared while the ring shows, so answering connects as fast as before — else over the WebSocket; callTransport, transport and transportFallbackReason apply to both directions.

GraineCalls.enableVoIPPushes

Call it on iOS when your own server rings customers: it hands you the VoIP push token to send the call push to, now and whenever it changes.

enableVoIPPushes(onToken: (token: string | null) => void): () => void
ParameterTypeDefaultMeaning
onToken(token: string | null) => voidrequiredCalled with the token — at once when the device already has one, and on every change — and with null when it was invalidated (stop sending to it).

Returns — a function that stops listening for token changes. On Android it does nothing and returns a function that does nothing: Android has no VoIP pushes — use handlePush.

Errors — throws the not-linked error on iOS in a build without the package.

What to expect — Every VoIP push must ring: iOS requires it, and the engine does. For calls Graine rings, you do not need this — the token is registered with Graine for you.

const off = GraineCalls.enableVoIPPushes((token) => myServer.saveVoipToken(token));

GraineCalls.handlePush

Call it on Android from your Firebase message handlers — background and foreground — with every push's data, first, when your app's own messaging service receives the pushes (@react-native-firebase/messaging): a Graine call rings at once, even when the app was not running. Without one, the package's own messaging service hands every push to the engine natively.

handlePush(data: Record<string, string>): Promise<boolean>
ParameterTypeDefaultMeaning
dataRecord<string, string>requiredThe push's data exactly as the messaging library hands it over.

Returns — Promise<boolean>: true when the push was a Graine call (it is ringing), false when it is not yours to ignore — handle your other pushes then. On iOS it resolves false: VoIP pushes arrive natively. Example value: true.

Errors — throws the not-linked error in a build without the package.

import messaging from "@react-native-firebase/messaging";
import { GraineCalls } from "@graineai/inapp-react-native-calls";
 
// A Graine call's push data is all strings.
messaging().setBackgroundMessageHandler(async (m) => {
  if (await GraineCalls.handlePush((m.data ?? {}) as Record<string, string>)) return;   // a Graine call: it rings
  // …your other pushes
});
messaging().onMessage((m) => GraineCalls.handlePush((m.data ?? {}) as Record<string, string>));

GraineCalls.registerPushToken

Call it on Android only when your app's own messaging service receives the tokens (@react-native-firebase/messaging), on every refresh (onTokenRefresh). With Firebase in the app the engine reads the token itself at launch, and without a messaging service of your own the package's receives every new one.

registerPushToken(push: { token: string; kind?: "fcm" | "voip"; environment?: "production" | "sandbox" }): Promise<boolean>
ParameterTypeDefaultMeaning
push.tokenstringrequiredThe push token.
push.kind"fcm" | "voip""fcm" on Android, "voip" on iOSWhat kind of token it is.
push.environment"production" | "sandbox"noneFor an APNs token you register yourself: which push environment it belongs to.

Returns — Promise<boolean>: true when the engine took the token — it keeps it and registers this device with Graine for every configured agent; on iOS, false for a VoIP token (iOS registers its own). Example value: true.

Errors — rejects with Error("registerPushToken needs the token") for an empty token, and with "@graineai/inapp-react-native-calls cannot register push tokens in this build: rebuild the app (pod install on iOS) — it carries native code, so an over-the-air update cannot add it." when the app's native module predates it.

import messaging from "@react-native-firebase/messaging";
import { GraineCalls } from "@graineai/inapp-react-native-calls";
 
// Only with @react-native-firebase/messaging, whose messaging service receives every token:
messaging().onTokenRefresh((token) => GraineCalls.registerPushToken({ token }));

GraineCalls.installId

Read it to see the id that makes this phone one of the customer's devices — the calls package and the in-app SDK share it, so a call and a chat are one device.

installId(): Promise<string | null>

Parameters — none.

Returns — Promise<string | null>: the id, or null before configure() or with a native module from before it. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none: a failure resolves null.

const id = await GraineCalls.installId();

GraineCalls.answer

Call it from your own ringing screen (callScreen: "app") to answer the ringing call; the system ring answers by itself.

answer(): Promise<void>

Parameters — none.

Returns — Promise<void>. The package asks for the microphone first; answered is reported and the call connects — over WebRTC in a build with WebRTC calls, unless callTransport or the dashboard turned it off (it was prepared while the call rang), else over the WebSocket; useGraineCall().transport says which. A refused microphone fails the call (iOS: "The microphone permission was not granted."; Android: the snapshot's error reads "The microphone permission was denied.").

Errors — throws the not-linked error in a build without the package.

<Button title="Answer" onPress={() => GraineCalls.answer()} />

GraineCalls.decline

Call it from your own ringing screen to decline the ringing call.

decline(): Promise<void>

Parameters — none.

Returns — Promise<void>; declined is reported. For a call Graine rang, nothing rings that customer again for 6 hours.

Errors — throws the not-linked error in a build without the package.

<Button title="Decline" onPress={() => GraineCalls.decline()} />

GraineCalls.onIncomingCall

Call it to hear a call start ringing — to draw your own ringing screen, or to log it.

onIncomingCall(fn: (c: GraineIncomingCall) => void): () => void
ParameterTypeDefaultMeaning
fn(c: GraineIncomingCall) => voidrequiredCalled with each ring — see GraineIncomingCall.

Returns — a function that stops listening.

Errors — throws the not-linked error in a build without the package.

What to expect — Rings that happened before anything listened (a call rung while the app's JavaScript was not running) reach the first listener, oldest first — up to 20 are kept.

const off = GraineCalls.onIncomingCall((c) => analytics.log("ring", { id: c.callId, agent: c.agent }));

GraineIncomingCall

Read it to show a ring: who is calling and why — it carries only what the ring shows, nothing personal.

FieldTypeDefaultMeaning
callIdstringrequiredThe call's id — Graine's for a call it rang, your server's for one you rang.
reasonstring | nullrequiredWhy the call is happening, from the push.
callerNamestring | nullrequiredThe name to show on the ring.
agentstring | nulloptionalThe configured agent calling, by your name for it, when the engine says.
hostedbooleanoptionaltrue when Graine rang this call itself — a call booked for this customer, or Ring now from the dashboard — rather than a push from your own server: its outcomes are reported to Graine for you, so your server has nothing to record. Absent on any other ring, as on the outcome.

GraineCalls.onOutcome

Call it once at launch to report every call's outcome to your server — answered, declined, missed, ended or failed.

onOutcome(fn: (o: GraineCallOutcome) => void): () => void
ParameterTypeDefaultMeaning
fn(o: GraineCallOutcome) => voidrequiredCalled with each outcome — see GraineCallOutcome.

Returns — a function that stops listening.

Errors — throws the not-linked error in a build without the package.

What to expect — Outcomes from before anything listened (a call that rang and ended while the app's JavaScript was not running) reach the first listener, oldest first — up to 20 are kept. A call Graine rang (hosted: true) is reported to Graine by the engine as well.

GraineCalls.onOutcome((o) => myServer.reportCall(o.callId, o.outcome));

Permissions and the setup check

What a call needs from the phone, and from your app's setup, in one place. The calls package never asks on its own at launch: it asks for the microphone when a call starts without it, and requestCallPermissions() asks earlier, when your app chooses — on Android from a transparent screen of the SDK's own, so nothing forwards a permission result. callSetupProblems() says what your setup still lacks; the native SDK logs each problem once from configure on (Xcode's console, logcat), and a development build also warns each once in Metro's console. A problem is never an exception: calls still work where they can. With Expo, the package's config plugin writes the iOS setup at prebuild (Permissions and capabilities).

GraineCalls.callPermissions

Read it to see which call permissions the phone holds now.

callPermissions(): Promise<GraineCallPermissions>

The microphone, and on Android the ring's notifications (13+ asks) and the full-screen ring (14+, Settings only); on iOS those two are "notNeeded" — CallKit rings and draws its own screen. Readable before configure.

Returns — a GraineCallPermissions. Read it again when the app returns to the foreground (AppState "active"): the customer may have changed Settings.

Errors — throws in a build without the package's native code, or with a native build older than this package: rebuild the app.

const p = await GraineCalls.callPermissions();
if (!p.allGranted) showFinishSetupCard();

GraineCalls.requestCallPermissions

Call it at a moment you choose — onboarding, a "Call me" screen — to ask for what a call needs.

requestCallPermissions(): Promise<GraineCallPermissions>

Asks for the call permissions the system's dialog can still ask for — the microphone, and notifications on Android 13+ — and resolves them as they stand after. Nothing to ask: it resolves at once. The full-screen ring has no dialog: openCallSettings("fullScreen") opens its page. The package never asks on its own at launch; a call that starts without the microphone still asks then.

Returns — a GraineCallPermissions, after the customer answers.

Errors — as callPermissions.

const p = await GraineCalls.requestCallPermissions();
if (p.fullScreen === "blocked") await GraineCalls.openCallSettings("fullScreen");

GraineCalls.openCallSettings

Call it to send the customer to the Settings page that changes a call permission.

openCallSettings(item: GraineCallPermission): Promise<boolean>

Opens, on Android, the full-screen ring's own page (14+) or the app's notification settings; otherwise the app's page, where the microphone is.

ParameterTypeDefaultMeaning
itemGraineCallPermissionrequired"microphone", "notifications" or "fullScreen".

Returns — true when a page opened.

Errors — throws Error('openCallSettings takes "microphone", "notifications" or "fullScreen", …') for any other word, and as callPermissions in a build without the native code.

await GraineCalls.openCallSettings("microphone");

GraineCalls.callSetupProblems

Read it to learn what your app's setup still lacks for calls, in plain words.

callSetupProblems(): Promise<GraineCallSetupProblem[]>

What the app's setup still lacks for calls, read now — each a stable code and a message with the exact fix. Empty when nothing is missing, and before configure.

CodePlatformWhen
microphone_usage_missingiOSNo NSMicrophoneUsageDescription: iOS closes the app when a call asks for the microphone. The Expo plugin adds it.
background_audio_missingiOSUIBackgroundModes lacks audio: a call goes silent when the screen locks.
background_voip_missingiOSUIBackgroundModes lacks voip: iOS does not wake the app for a ring.
push_entitlement_missingiOSSigned without Push Notifications (read from development and ad hoc builds).
voip_pushes_not_enablediOSenableVoIPPushes not called 15 seconds after configure.
no_voip_tokeniOSNo VoIP token 15 seconds after enableVoIPPushes (never on the simulator).
manifest_entry_missingAndroidThe merged manifest lost a call entry (look for tools:node="remove").
no_push_tokenAndroidNo push token 15 seconds after configure: Firebase is missing or not set up in the Android app (the message says which), or an app whose own messaging service receives the tokens never called registerPushToken. Calls can't ring.
notifications_not_allowedAndroidA call can't ring while the app is closed or in the background.
full_screen_not_allowedAndroidA locked phone rings with a heads-up notification instead.
microphone_blockedBothOnly Settings can let calls hear the customer.
hosted_ringing_offBothNo push credentials uploaded for this platform: Graine can't ring this phone.

Returns — GraineCallSetupProblem[].

Errors — as callPermissions.

for (const p of await GraineCalls.callSetupProblems()) console.warn(p.code, p.message);

GraineCallPermission

Use it to name a call permission.

type GraineCallPermission = "microphone" | "notifications" | "fullScreen";
ValueMeaning
"microphone"The customer's voice.
"notifications"A call that rings while the app is closed or in the background — Android (13+ asks). iOS rings through CallKit, which needs none.
"fullScreen"A ring that fills a locked screen — Android 14+, allowed in Settings. iOS: CallKit's own screen.

GraineCallPermissionStatus

Read it to tell whether a call permission is allowed, can still be asked for, or only Settings can change it.

type GraineCallPermissionStatus = "granted" | "denied" | "blocked" | "notNeeded";
ValueMeaning
"granted"Allowed.
"denied"Not allowed yet: requestCallPermissions() can ask for it. A word the engine sends that this package does not know reads "denied", never granted.
"blocked"Refused for good, or only allowed in Settings: offer openCallSettings.
"notNeeded"This phone needs no such permission.

GraineCallPermissions

Read it to see every call permission at once.

FieldTypeDefaultMeaning
microphoneGraineCallPermissionStatusrequiredThe microphone.
notificationsGraineCallPermissionStatusrequiredThe ring's notifications — "notNeeded" on iOS.
fullScreenGraineCallPermissionStatusrequiredThe full-screen ring — "notNeeded" on iOS.
allGrantedbooleanrequiredEvery permission is granted or not needed: calls can ring and connect.

GraineCallSetupProblem

Read it to show or log what your app's setup still lacks.

FieldTypeDefaultMeaning
codestringrequiredStable, the same on every SDK: act on it, or count it. The list is under GraineCalls.callSetupProblems.
messagestringrequiredWhat is missing and how to fix it, in plain words.

Booking with the SDK's sheet

Use these to let a customer book a call — "call me at…" — with no screen of your own: the SDK draws the booking sheet in the agent's design, books the call, and tells you what was booked. The same rules are exported for an app that draws its own booking screen. New in 0.42.0.

What the customer sees. The sheet's header reads Book a call, with "call name will call you" under it — the agent's call name from the dashboard, else the chat's title, else the heading, else "Assistant" — and a close button. Under Day, a chip per day from today to the last day the agent allows: "Today", "Tomorrow", then the weekday and date in the phone's words ("Thu 16 Oct"); a day with no time left is not offered. Under Time, a grid of times every 15 minutes, four to a row, only between the earliest time the agent allows (now plus its notice, rounded up to 5 minutes) and the latest (now plus its days ahead, rounded down to 5 minutes); the day's first time is chosen. Under the grid, the phone's time zone and the time chosen — "Asia/Kolkata · Tue 14 Oct, 6:00 PM". Then What's it about? (optional), one line of up to 120 characters (line breaks become spaces), filled with your reason when you pass one. Then Book call, which shows a spinner while it books.

A refusal stays in the sheet as one line under Book call, worded from the agent's own numbers:

CodeThe line
too_soon"Pick a time at least 5 minutes from now" — the agent's notice; the sheet then moves to the new earliest time.
too_far"Pick a time within 14 days" — the agent's limit.
outside_window"That time is outside the hours we can call — pick another"
too_many_pending"You already have 3 calls booked — cancel one first" — the agent's limit.
scheduling_off"Booking a call isn't available right now"
identity_required"Sign in to book a call"
rate_limited, unavailable, network, and any other code"Couldn't book just now — try again"

Opened where booking is not available, the sheet opens, shows the scheduling_off or identity_required line and does not book. Once booked, it says "Booked — call name will call you on Wed 14 Oct, 6:00 PM" (always the date, never "Today") with Done. It closes with Done, the close button, a tap beside it, or Android's back button; closed without booking, it books nothing. On a phone it is a sheet from the bottom with a grab handle; on a tablet (shorter side 600 points or more), a centred panel 420 points wide. The times move on while it is open: what is offered is checked again every 30 seconds and when the app returns to the foreground.

In the SDK's chat windows (GraineVoiceBar's chat and GraineChatScreen) — new in 0.42.0, with the Upcoming calls card itself — where booking is available and the design keeps the booked-calls card: Book a call at the foot of the card under the customer's booked calls, or — when nothing is booked — a compact row of its own in the card's place reading Book a call and "call name will call you". Either opens the sheet over the chat, and the card reads the calls again after a booking.

GraineBookingSheet

Use it when your own "Call me" button should open the booking sheet and you want to hold its open state yourself; otherwise call openBooking() and the provider draws it.

function GraineBookingSheet(props: GraineBookingSheetProps): JSX.Element | null

Props — GraineBookingSheetProps.

Returns — the sheet while visible is true (a fresh one each time it opens — no day, time or error left from the last), else null.

Errors — none thrown: with no client at all (no provider, no default client, none passed) it shows "Booking a call isn't available right now" and does not book. A booking's refusal is shown in the sheet, never thrown.

What to expect — Until the agent's settings have loaded it asks for them, and Book call stays disabled; then it offers what the agent's rules allow and shows the refusal line when booking is not available here. It is redrawn when the settings arrive and when the customer signs in or out.

const [open, setOpen] = useState(false);
const { bookingAvailable } = useGraineAgent();
const insets = useSafeAreaInsets();
{bookingAvailable && <Button title="Call me" onPress={() => setOpen(true)} />}
<GraineBookingSheet visible={open} onClose={() => setOpen(false)} onBooked={(call) => console.log("Booked", call.id, call.at)} reason="Talk through my order" safeArea={insets} />

GraineBookingSheetProps

Read it to show the sheet, hear what it booked, and fill in what the call is about.

PropTypeDefaultMeaning
visiblebooleanrequiredtrue shows the sheet; false hides it.
onClose() => voidrequiredHear the sheet close — Done, the close button, a tap beside it, Android's back — and set visible to false.
onBooked(call: GraineScheduledCall) => voidnoneHear the call that was booked, the moment it is; the sheet stays open saying so until Done. See GraineScheduledCall.
reasonstringnoneFills "What's it about? (optional)"; the customer may change it. Trimmed and cut to 120 characters.
safeArea{ top?: number; bottom?: number }a typical phone'sThe phone's safe areas, so the sheet clears the notch and the home indicator — useSafeAreaInsets().
clientGraineInAppClientthe provider's, else the default clientThe agent to book with — for an app with several agents.

GraineInAppClient.openBooking

Call it from your own "Call me" button to open the SDK's booking sheet — the provider draws it — and get back the call the customer booked.

openBooking(options?: { reason?: string }): Promise<GraineScheduledCall | null>
ParameterTypeDefaultMeaning
options.reasonstringnoneFills "What's it about? (optional)"; trimmed and cut to 120 characters; the customer may change it.

Returns — Promise<GraineScheduledCall | null>: when the sheet closes, the call the customer booked — see GraineScheduledCall — or null when they closed it without booking. Example value: { id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: new Date("2026-10-16T12:30:00Z"), status: "scheduled", source: "app", reason: "Talk through my order", isUpcoming: true, canCancel: true }.

Errors — none: it never rejects. Refusals are shown in the sheet.

What to expect — A GraineProvider with this client draws the sheet — one mounted later still does, and only one draws it. A second openBooking() while one is open replaces it; the first resolves null. dispose() resolves an open one null. Opened where booking is not available, the sheet explains why and resolves null when closed — check bookingAvailable to hide your button instead. Announced as the open_booking event ({ id, reason }). The same function is openBooking in useGraineAgent() and useGraineScheduling().

const { bookingAvailable, openBooking } = useGraineAgent();
{bookingAvailable && (
  <Button title="Call me" onPress={async () => {
    const call = await openBooking({ reason: "Talk through my order" });
    if (call) showToast(`Booked for ${call.at?.toLocaleString()}`);
  }} />
)}

GraineInAppClient.bookingAvailable

Read it to show or hide your own "Call me" button: it is true only when this customer can book a call from here.

get bookingAvailable(): boolean

Returns — true when all of these hold:

  • the agent lets customers book (schedulingRules.usersCanBook);
  • Graine can ring this phone: your workspace has In-app calling set up (calls.hosted) and its platforms are empty or name this one (ios or android);
  • the customer is identified — identify() with a real value for the trait the agent's Who is the user? setting names.

false until the agent's settings have loaded. Example value: true.

Errors — none.

What to expect — It changes when the settings load (session event) and when the customer signs in or out (identity event); useGraineAgent() and useGraineScheduling() carry it as state, so a component redraws by itself. Each agent has its own.

const [showCallMe, setShowCallMe] = useState(client.bookingAvailable);
useEffect(() => client.on("identity", () => setShowCallMe(client.bookingAvailable)), []);

GraineInAppClient.bookingBlocker

Read it to say why booking is not available — the same reason the sheet shows.

get bookingBlocker(): BookingBlocker | null

Returns — "scheduling_off" (the agent takes no bookings, or Graine cannot ring this phone), "identity_required" (nobody is identified), or null when bookingAvailable is true. The agent's settings come first: signing in would not help while booking is off. Example value: "identity_required".

Errors — none.

if (client.bookingBlocker === "identity_required") showSignInToBook();

GraineInAppClient.takeBooking

The provider calls it to take the sheet an openBooking() asked for, once — so two providers of one client draw one sheet. Your app does not need it.

takeBooking(): { id: number; reason: string | null } | null

Parameters — none.

Returns — the open sheet's id and reason, the first time it is asked; null when there is none or it is already taken. Example value: { id: 3, reason: "Talk through my order" }.

Errors — none.

const ask = client.takeBooking();

GraineInAppClient.settleBooking

The provider calls it when the sheet it drew closes, to resolve that openBooking(). Your app does not need it.

settleBooking(id: number, call: GraineScheduledCall | null): void
ParameterTypeDefaultMeaning
idnumberrequiredThe sheet's id, from takeBooking(). A sheet that is no longer the open one is ignored.
callGraineScheduledCall | nullrequiredThe call booked, or null.

Returns — nothing.

Errors — none.

const ask = client.takeBooking();              // an openBooking() still waiting for its sheet
if (ask) client.settleBooking(ask.id, null);   // closed without booking: that openBooking() resolves null

bookingAvailableFrom

Use it to apply the booking rule yourself, from the agent's settings — what bookingAvailable computes.

function bookingAvailableFrom(calls: unknown, platform: string | null | undefined, identified: boolean): boolean
ParameterTypeDefaultMeaning
callsunknownrequiredThe agent's calls settings — client.config?.calls.
platformstring | null | undefinedrequired"ios", "android" or "web" — see BookingPlatform.
identifiedbooleanrequiredWhether the customer is identified with the agent's identifying trait.

Returns — true when bookingBlocker(…) is null. Example value: false.

Errors — none.

bookingAvailableFrom(client.config?.calls, Platform.OS, true);

bookingBlocker

Use it to find why booking is not offered, from the agent's settings.

function bookingBlocker(calls: unknown, platform: string | null | undefined, identified: boolean): BookingBlocker | null
ParameterTypeDefaultMeaning
callsunknownrequiredThe agent's calls settings.
platformstring | null | undefinedrequiredWhere the customer books from. A web page cannot be rung, so on "web" the ringing rule does not apply — the booking rings the customer's app. Any other value than "ios", "android" or "web" is not bookable.
identifiedbooleanrequiredWhether the customer is identified.

Returns — "scheduling_off" when calls.scheduling.usersCanBook is not true, or (on a phone) calls.hosted is not true or calls.platforms names other platforms only; then "identity_required" when identified is false; else null. Example value: "scheduling_off".

Errors — none.

bookingBlocker({ hosted: true, scheduling: { usersCanBook: true } }, "android", false); // "identity_required"

BookingBlocker

Read it as why booking is not offered here — the code whose line the sheet shows.

type BookingBlocker = "scheduling_off" | "identity_required";
ValueMeaning
"scheduling_off"The agent takes no bookings from the app, or Graine cannot ring this phone. The sheet says "Booking a call isn't available right now".
"identity_required"Nobody is identified. The sheet says "Sign in to book a call".

BookingPlatform

Read it as where a customer books from.

type BookingPlatform = "ios" | "android" | "web";
ValueMeaning
"ios"An iPhone or iPad Graine can ring.
"android"An Android phone Graine can ring.
"web"A web page: it cannot be rung, so the booking rings the customer's app.

bookingBounds

Use it to find the earliest and latest times a booking may be made for, rounded as the sheet offers them.

function bookingBounds(rules: { minLeadMinutes: number; maxDaysAhead: number }, now: Date = new Date()): BookingBounds
ParameterTypeDefaultMeaning
rules{ minLeadMinutes: number; maxDaysAhead: number }requiredThe agent's notice and horizon — client.schedulingRules. A number that is not positive counts as 0.
nowDatenowThe moment to count from.

Returns — a BookingBounds. Example value: { earliest: new Date("2026-10-15T10:10:00Z"), latest: new Date("2026-10-29T10:05:00Z") }.

Errors — none.

const { earliest, latest } = bookingBounds(client.schedulingRules);

BookingBounds

Read it as the window every time the sheet offers lies in, both ends on the 5-minute grid.

FieldTypeDefaultMeaning
earliestDaterequiredNow plus the agent's notice (minLeadMinutes), rounded up to 5 minutes — the first time that may be booked.
latestDaterequiredNow plus the agent's days ahead (maxDaysAhead), rounded down to 5 minutes — the last time that may be booked.

clampBooking

Use it to keep a time a customer chose in your own picker inside the bounds.

function clampBooking(at: Date, bounds: BookingBounds): Date
ParameterTypeDefaultMeaning
atDaterequiredThe time chosen.
boundsBookingBoundsrequiredFrom bookingBounds.

Returns — at when it lies inside; earliest for a time before it or a date that cannot be read; latest for a time after it (never before earliest). Example value: new Date("2026-10-15T10:10:00Z").

Errors — none.

const at = clampBooking(pickerValue, bookingBounds(client.schedulingRules));

bookingDays

Use it to list the days a booking screen of your own should offer, each with its first and last time.

function bookingDays(bounds: BookingBounds, now: Date = new Date(), options: { stepMinutes?: number; utcOffsetMinutes?: number } = {}): BookingDay[]
ParameterTypeDefaultMeaning
boundsBookingBoundsrequiredFrom bookingBounds.
nowDatenowThe moment "Today" and "Tomorrow" are measured from.
options.stepMinutesnumber5The grid the times sit on: 5 for a time picker, BOOKING_SLOT_MINUTES (15) for a grid of times.
options.utcOffsetMinutesnumberthe phone's zoneA fixed offset east of UTC, in minutes, to read days in.

Returns — one BookingDay per day from the day of the earliest time to the last bookable day, in order; a day with nothing left on the grid is left out. Example value: [{ key: "2026-10-15", label: "Today", first: …, last: … }, { key: "2026-10-16", label: "Tomorrow", … }].

Errors — none.

const days = bookingDays(bookingBounds(client.schedulingRules), new Date(), { stepMinutes: BOOKING_SLOT_MINUTES });

BookingDay

Read it as one day the booking screen offers.

FieldTypeDefaultMeaning
keystringrequiredThe day on the phone, YYYY-MM-DD — a stable key for the chip.
labelstringrequiredThe chip's words: "Today", "Tomorrow", then the weekday and date ("Thu 16 Oct").
firstDaterequiredThe first time the day offers — on the grid, inside the bounds.
lastDaterequiredThe last time the day offers.

bookingSlots

Use it to list the times of one day for a grid of times.

function bookingSlots(day: BookingDay, stepMinutes: number = BOOKING_SLOT_MINUTES): Date[]
ParameterTypeDefaultMeaning
dayBookingDayrequiredThe day, from bookingDays.
stepMinutesnumber15The gap between times, on the clock's grid (:00, :15, :30, :45).

Returns — the times from the day's first to its last, every stepMinutes. Example value: [new Date("…T10:15:00"), new Date("…T10:30:00"), …].

Errors — none.

const days = bookingDays(bookingBounds(client.schedulingRules));
const times = bookingSlots(days[0]);

bookingDayLabel

Use it to word a day's chip: "Today", "Tomorrow", else the date.

function bookingDayLabel(at: Date, now: Date = new Date(), utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
atDaterequiredA moment on the day.
nowDatenowThe moment "Today" is measured from.
utcOffsetMinutesnumberthe phone's zoneA fixed offset east of UTC, in minutes.

Returns — "Today", "Tomorrow", or the weekday and date in the phone's words (with the year when it is not this year). Example value: "Thu 16 Oct".

Errors — none.

const [day] = bookingDays(bookingBounds(client.schedulingRules));
bookingDayLabel(day.first);

bookingCallName

Use it to choose the name in "… will call you", as every platform's sheet does.

function bookingCallName(...names: Array<string | null | undefined>): string
ParameterTypeDefaultMeaning
namesArray<string | null | undefined>noneCandidate names in order — the agent's call name (schedulingRules.callName), the chat's title, the heading.

Returns — the first that is not blank, trimmed; "Assistant" when none is. Example value: "Acme".

Errors — none.

const theme = useGraineTheme();
const { appearance } = useGraineAgent();
bookingCallName(client.schedulingRules.callName, theme.chat.title, appearance?.heading);

bookingSubtitle

Use it to word the line under the sheet's title.

function bookingSubtitle(callName: string): string
ParameterTypeDefaultMeaning
callNamestringrequiredFrom bookingCallName.

Returns — the name followed by " will call you". Example value: "Acme will call you".

Errors — none.

bookingSubtitle("Acme");

bookingZoneLine

Use it to word the time-zone line under the times: the zone, then the time chosen.

function bookingZoneLine(at: Date, zone: string | null = deviceTimezone(), now: Date = new Date(), utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
atDaterequiredThe time chosen.
zonestring | nullthe phone's IANA zoneThe zone to name; null leaves it out.
nowDatenowDecides whether the year is shown.
utcOffsetMinutesnumberthe phone's zoneA fixed offset east of UTC, in minutes.

Returns — the zone, " · ", then the date and time in the phone's words. Example value: "Asia/Kolkata · Tue 14 Oct, 6:00 PM".

Errors — none.

bookingZoneLine(new Date(Date.now() + 60 * 60_000));

bookingDoneLine

Use it to word the confirmation after a booking.

function bookingDoneLine(callName: string, at: Date | null, now: Date = new Date(), utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
callNamestringrequiredFrom bookingCallName.
atDate | nullrequiredWhen the call rings — the booked call's at.
nowDatenowDecides whether the year is shown.
utcOffsetMinutesnumberthe phone's zoneA fixed offset east of UTC, in minutes.

Returns — "Booked — " and the name, " will call you on " and the date and time (always the date, never "Today"); without a time, "Booked — name will call you". Example value: "Booked — Acme will call you on Wed 14 Oct, 6:00 PM".

Errors — none.

const call = await client.scheduleCall(new Date(Date.now() + 60 * 60_000));
bookingDoneLine("Acme", call.at);

bookingErrorLine

Use it to word a booking refusal in one plain line, from the error's code and the agent's numbers.

function bookingErrorLine(code: unknown, rules: { minLeadMinutes: number; maxDaysAhead: number; maxPendingPerUser: number }): string
ParameterTypeDefaultMeaning
codeunknownrequiredThe GraineError's code.
rules{ minLeadMinutes: number; maxDaysAhead: number; maxPendingPerUser: number }requiredThe agent's numbers — client.schedulingRules.

Returns — the line from the refusal table at the top of this section, with the agent's numbers ("1 minute" or "5 minutes"); "Couldn't book just now — try again" for any other code. Example value: "Pick a time at least 5 minutes from now".

Errors — none.

const [line, setLine] = useState<string | null>(null);
try { await client.scheduleCall(at, about); } catch (e) { setLine(bookingErrorLine((e as GraineError).code, client.schedulingRules)); }

BOOKING_TEXT

Use it to word your own booking screen exactly as the SDK's sheet does.

const BOOKING_TEXT = { title: "Book a call", about: "What's it about? (optional)", book: "Book call", done: "Done", web: "We'll ring you in the app" } as const;
KeyValueWhere it shows
title"Book a call"The sheet's title, and the chat's "Book a call" button and row.
about"What's it about? (optional)"The label of the line for what the call is about.
book"Book call"The button that books.
done"Done"The button that closes the sheet after a booking.
web"We'll ring you in the app"On a website, under the times: the booking rings the customer's app, not the browser.

BOOKING_ROUND_MINUTES

Read it as the grid booking times sit on: the earliest time is rounded up to it, the latest down.

const BOOKING_ROUND_MINUTES = 5;

What to expect — 5 minutes.

BOOKING_SLOT_MINUTES

Read it as the gap between the times in React Native's grid of times (and the website's).

const BOOKING_SLOT_MINUTES = 15;

What to expect — 15 minutes: :00, :15, :30 and :45.

BOOKING_REASON_MAX

Read it as the longest "what's it about" a booking keeps.

const BOOKING_REASON_MAX = 120;

What to expect — 120 characters; the sheet stops typing there, and scheduleCall and openBooking cut a longer reason.

The scheduled-calls list

Use these to read what a customer may book, book a call from a screen of your own, list the customer's calls and cancel one. How booked calls ring — who may book, when they ring, every status — is Scheduled calls.

The SDK lists them for you, too. Since 0.42.0 the SDK's chat windows (GraineVoiceBar's chat and GraineChatScreen) open with an Upcoming calls card: the customer's booked calls still to ring, soonest first, three at most — each with its time ("Tomorrow, 9:00 AM"), the call's name and Cancel ("Cancelling…", then gone; "Couldn't cancel · tap to retry" when it fails). The card is read when the chat opens, after a booking or a cancellation through the SDK, when the customer changes, when the app returns to the foreground, and after the agent's turns at most every 30 seconds; a read that fails leaves it as it was. The design's chat.showUpcomingCalls hides it and chat.upcomingTitle titles it ("Upcoming call" / "Upcoming calls" by default).

GraineInAppClient.schedulingRules

Read it to build a booking screen within the agent's rules: whether customers may book, how soon and how far ahead, and how many calls may wait.

get schedulingRules(): GraineSchedulingRules

Returns — a GraineSchedulingRules, read from the agent's settings. Before they load, booking is off and the numbers are the defaults. Example value: { usersCanBook: true, minLeadMinutes: 5, maxDaysAhead: 14, maxPendingPerUser: 3, ringTimeoutSeconds: 45, callName: "Acme", hostedRinging: true, platforms: ["ios", "android"], earliest: [Function], latest: [Function] }.

Errors — none.

const rules = client.schedulingRules;
if (rules.usersCanBook) showPicker({ min: rules.earliest(), max: rules.latest() });

GraineSchedulingRules

Read it as what a customer may book, as the agent's Calls & scheduling settings say.

What to expect — A property, not a request: read from the agent's settings when they load; until then booking is off. scheduleCall checks the first three rules on the device before anything is sent; Graine then checks the calling hours and the calls already waiting.

FieldTypeDefaultMeaning
usersCanBookbooleanfalseWhether customers may book calls from the app at all.
minLeadMinutesnumber5The least notice a booking needs, in minutes.
maxDaysAheadnumber14How far ahead a call may be booked, in days.
maxPendingPerUsernumber3How many booked calls one customer may have waiting.
ringTimeoutSecondsnumber45How long Graine rings a booked call, in seconds (read in the dashboard's range, 20–60).
callNamestring | nullnullThe name the ring shows — and the booking sheet's "… will call you".
hostedRingingbooleanfalseWhether Graine rings the customer's devices itself — your workspace has In-app calling set up.
platformsArray<"ios" | "android">[]The platforms Graine rings on; empty means every platform.
earliest(now?: Date) => Date—The earliest time a call may be booked for: now plus minLeadMinutes.
latest(now?: Date) => Date—The latest: now plus maxDaysAhead days.

GraineInAppClient.scheduleCall

Call it from a booking screen of your own to book a call from the agent for a chosen time; the SDK's own sheet uses it too.

async scheduleCall(at: Date | string | number, reason?: string): Promise<GraineScheduledCall>
async scheduleCall(request: { at: Date | string | number; reason?: string }): Promise<GraineScheduledCall>
ParameterTypeDefaultMeaning
atDate | string | numberrequiredWhen the call should ring: a Date, an ISO-8601 time, or milliseconds since 1970. Sent as a moment with the customer's time zone; it must lie between schedulingRules.earliest() and latest().
reasonstringnoneWhat the call is about — kept with the call for the agent (it hears it as call_reason when it rings), never in the push. Trimmed and cut to 120 characters.
request{ at: Date | string | number; reason?: string }—The same two, as one object.

Returns — Promise<GraineScheduledCall>: the booked call, status "scheduled" — see GraineScheduledCall. It rings the identified customer's devices (or, for someone not identified, this install) when it is due, inside the agent's calling hours. Announced as scheduled_calls_changed ({ booked: id }). Example value: { id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: new Date("2026-10-16T12:30:00Z"), status: "scheduled", source: "app", reason: "Talk through my order", isUpcoming: true, canCancel: true }.

Errors — rejects with a GraineError. Checked on the device first, before anything is sent (status 0) — the refusals in the booking sheet's own words, as on every SDK and the website:

  • invalid_request — "at must be a Date, an ISO-8601 time or a timestamp."
  • scheduling_off — "Booking a call isn't available right now"
  • too_soon — "Pick a time at least 5 minutes from now" (the agent's notice — "1 minute" when it is one; 30 seconds of grace are allowed)
  • too_far — "Pick a time within 14 days"
  • invalid_request — the device has no install id (the message names storage and the calls package).

Then from Graine: outside_window (outside the agent's calling hours in the customer's zone — the message names a time that works), too_many_pending, identity_required, device_limit, invalid_request, rate_limited, unavailable, invalid_key, origin_refused; network — "Could not reach Graine. Check the connection and try again."; unknown — "The call was booked, but its details did not come back. Read them with scheduledCalls()."

try {
  const call = await client.scheduleCall(new Date(Date.now() + 60 * 60_000), "Talk through my order");
} catch (e) {
  if (e instanceof GraineError) showLine(bookingErrorLine(e.code, client.schedulingRules));
}

GraineInAppClient.scheduledCalls

Call it to list the customer's booked calls — for a "Your calls" screen.

async scheduledCalls(): Promise<GraineScheduledCall[]>

Parameters — none.

Returns — Promise<GraineScheduledCall[]>: this customer's calls (or this install's, for someone not identified), upcoming first. Example value: [{ id: "6f1c2a9e-…", at: new Date("2026-10-16T12:30:00Z"), status: "scheduled", source: "agent", reason: null, isUpcoming: true, canCancel: true }].

Errors — rejects with a GraineError: invalid_request (no install id), network, rate_limited, unavailable, invalid_key, origin_refused.

const calls = await client.scheduledCalls();

GraineInAppClient.cancelScheduledCall

Call it from a booked call's Cancel button; only a call that has not started ringing (canCancel) can be cancelled.

async cancelScheduledCall(id: string): Promise<GraineCallStatus>
ParameterTypeDefaultMeaning
idstringrequiredThe booked call's id.

Returns — Promise<GraineCallStatus>: the call's status afterwards — "cancelled". Announced as scheduled_calls_changed ({ cancelled: id }). Example value: "cancelled".

Errors — rejects with a GraineError: invalid_request — "Pass the id of the call to cancel." for an empty id, or no install id; not_found — not this customer's or install's, or no such call; network, rate_limited, unavailable.

const [call] = await client.scheduledCalls();
if (call?.canCancel) await client.cancelScheduledCall(call.id);

useGraineScheduling

Call it in a screen that books and lists calls: the rules, the customer's calls, and booking and cancelling, as state — plus the SDK's own sheet.

function useGraineScheduling(): {
  rules: GraineSchedulingRules;
  calls: GraineScheduledCall[];
  schedule: (at: Date | string | number, reason?: string) => Promise<GraineScheduledCall>;
  cancel: (id: string) => Promise<GraineCallStatus>;
  refresh: () => Promise<void>;
  loading: boolean;
  error: GraineError | null;
  bookingAvailable: boolean;
  openBooking: (options?: { reason?: string }) => Promise<GraineScheduledCall | null>;
}

Parameters — none.

Returns — rules: the agent's rules, following its settings as they load; calls: the customer's calls, upcoming first — read on mount, after every booking or cancellation, and when the customer changes; schedule and cancel: as scheduleCall and cancelScheduledCall — they reject with a GraineError and also set error; refresh(): read the calls again (it never rejects — a failure lands in error); loading: a read is in progress; error: the last failure, else null; 0.42.0: bookingAvailable and openBooking — see Booking with the SDK's sheet. Example value: { rules: { usersCanBook: true, … }, calls: [], loading: false, error: null, bookingAvailable: true, … }.

Errors — throws, like useGraineClient, when there is neither a provider above nor a default client.

const { rules, calls, cancel, bookingAvailable, openBooking } = useGraineScheduling();
return (
  <>
    {calls.filter((c) => c.isUpcoming).map((c) => (
      <Row key={c.id} title={upcomingTimeLabel(c.at)} onCancel={c.canCancel ? () => cancel(c.id) : undefined} />
    ))}
    {bookingAvailable && <Button title="Book a call" onPress={() => openBooking()} />}
  </>
);

GraineScheduledCall

Read it to show a booked call: when it rings, where it is, and whether it can still be cancelled.

What to expect — From scheduleCall (status scheduled), scheduledCalls() (upcoming first) and the chat's booked-calls card. at is a moment in time — show it in the phone's time zone. A status or source newer than this SDK arrives as its own word: give every switch a default.

FieldTypeDefaultMeaning
idstringrequiredThe booked call's id (a UUID) — pass it to cancelScheduledCall.
atDate | nullrequiredWhen it rings; null only when Graine's time could not be read.
statusGraineCallStatusrequiredWhere the call is — see GraineCallStatus.
sourceGraineCallSource | nullrequiredWho booked it — see GraineCallSource; null when Graine did not say.
reasonstring | nullrequiredWhat the call is about, at most 120 characters; null when none was given.
isUpcomingbooleanrequiredtrue while it has not rung yet, or is ringing now (scheduled or ringing).
canCancelbooleanrequiredtrue while it can still be cancelled — it has not started ringing (scheduled).

GraineCallStatus

Read it as where a booked call is.

type GraineCallStatus = "scheduled" | "ringing" | "answered" | "completed" | "declined" | "missed" | "failed" | "cancelled" | "expired" | "noDevice" | (string & {});

What to expect — scheduled → ringing → answered → completed, or one of the endings below. A status newer than this SDK arrives as its own word.

ValueMeaning
"scheduled"Booked, waiting for its time — it can be cancelled.
"ringing"Ringing the customer's devices now.
"answered"The customer answered; the conversation is under way.
"completed"The call happened and ended.
"declined"The customer declined the ring; nothing rings that customer again for 6 hours.
"missed"It rang out unanswered.
"failed"It could not be placed.
"cancelled"Called off before it rang.
"expired"Its time passed while it could not ring — more than 10 minutes late, or no opening left in the calling hours.
"noDevice"No device of the customer's has been seen in the last 30 days (a browser cannot be rung).

GraineCallSource

Read it as who booked a call.

type GraineCallSource = "agent" | "app" | "dashboard" | "api" | (string & {});
ValueMeaning
"agent"The agent booked it, in a conversation.
"app"Your app booked it — scheduleCall or the booking sheet.
"dashboard"Your team booked it in the dashboard.
"api"It was booked through the API.

A source newer than this SDK arrives as its own word.

upcomingCalls

Use it to pick the calls a booked-calls card lists: those still to ring, soonest first.

function upcomingCalls(calls: readonly GraineScheduledCall[] | null | undefined): GraineScheduledCall[]
ParameterTypeDefaultMeaning
callsreadonly GraineScheduledCall[] | null | undefinedrequiredThe customer's calls — scheduledCalls().

Returns — the scheduled calls only, soonest first, a call with no readable time last. The SDK's card draws the first three. Example value: [{ id: "6f1c…", status: "scheduled", … }].

Errors — none.

const calls = await client.scheduledCalls();
const shown = upcomingCalls(calls).slice(0, 3);

upcomingTimeLabel

Use it to word a booked call's time on a card.

function upcomingTimeLabel(at: Date | null, now: Date = new Date(), utcOffsetMinutes?: number): string
ParameterTypeDefaultMeaning
atDate | nullrequiredWhen the call rings.
nowDatenowThe moment "Today" is measured from.
utcOffsetMinutesnumberthe phone's zoneA fixed offset east of UTC, in minutes.

Returns — "Today, 5:30 PM", "Tomorrow, 9:00 AM", else the date and time ("Tue 14 Oct, 10:00 AM"), in the phone's words; "Booked" when there is no time. Example value: "Tomorrow, 9:00 AM".

Errors — none.

const [call] = await client.scheduledCalls();
if (call) upcomingTimeLabel(call.at);

Design

Use these to draw your own UI beside the agent's in the same design the brand chose in the dashboard (the Garage), and to read the design system the SDK draws with: tokens, presets, icons, logos, typefaces, appearance and the chat window's sizes.

useGraineTheme

Call it in any component to draw in the agent's design — the same colours, type, corners and depth the SDK draws with.

function useGraineTheme(): ThemeTokens

Parameters — none.

Returns — the ThemeTokens for now: the brand's fixed light or dark, else the phone's, picked again the moment the phone switches appearance. Outside a provider, the default design. Example value: { v: 1, preset: "signature", scheme: "light", colors: { accent: "#3740B8", … }, … }.

Errors — none.

const t = useGraineTheme();
<View style={{ backgroundColor: t.colors.surface, borderRadius: t.shape.radius.lg, padding: t.spacing.cardPadding }}>
  <Text style={{ color: t.colors.text, fontSize: t.font.scale.body }}>Your plan</Text>
</View>

ThemeTokens

Read it as everything a renderer needs for one scheme, as plain numbers and #RRGGBB colours.

What to expect — Resolved by Graine from the brand's design and carried with the agent's settings; a token missing from an older server is filled with the default. Every text colour has already been checked against what it sits on.

FieldTypeDefaultMeaning
v1requiredThe token format's version.
presetstringrequiredThe design preset (philosophy) these tokens come from — see DESIGN_PRESETS.
scheme"light" | "dark"requiredWhich scheme these tokens are for.
colorsThemeColorsrequiredThe colours — see ThemeColors.
font{ body: string; heading: string; scale: { caption: number; body: number; title: number; display: number }; weights: { body: number; strong: number; title: number }; letterSpacing: { body: number; title: number }; lineHeight: { body: number; title: number } }requiredType: the body and heading families ("system" is the phone's own font), sizes in points, weights, letter spacing and line height.
shape{ radius: { sm: number; md: number; lg: number; xl: number }; borderWidth: number; launcher: LauncherShape }requiredCorner radii in points (at most 48), the hairline width, and the launcher's shape.
spacing{ unit: number; cardPadding: number; gap: number }requiredThe spacing unit, a card's padding and the gap between items, in points.
elevation{ card: ThemeShadow | null; floating: ThemeShadow | null; hardOffset: { x: number; y: number } | null }requiredShadows for cards and floating surfaces; hardOffset is a solid offset shadow drawn in colors.text (a bold, outlined look).
motion{ durationMs: number; easing: MotionEasing; bounce: number }requiredHow long transitions take, their easing, and how much they overshoot.
icons{ style: IconStyle; stroke: number; launcher: { id: string; path: string; stroke: boolean } | null }requiredThe icon style and stroke width, and the launcher's glyph (null when the launcher shows the brand's logo).
glass{ blur: number; surfaceOpacity: number } | nullrequiredTranslucent, blurred surfaces, or null for solid ones.
bubblesDesignBubblesrequiredHow message bubbles are drawn.
chromeDesignChromerequired"window" frames the chat, the caption and components as desktop windows; "none" is a plain surface.
chatThemeChatrequiredThe chat window, resolved: its words (title, subtitle, placeholder — empty for the defaults), avatar, timestamps, conversation starters, background and pattern, header and composer colours, size, and the pieces it may leave out — showUpcomingCalls, upcomingTitle, showHistory, newConversation (all on by default).
callThemeCallrequiredThe call screen, resolved: its background (and gradient), text, avatar, button colours, end-button shape, whether captions and a timer show, the in-call controls, the ringtone and the status words.

ThemeColors

Read it as the design's colours for one scheme, each a #RRGGBB string.

FieldTypeDefaultMeaning
backgroundstringrequiredThe page behind everything.
surfacestringrequiredPanels, sheets and the bar.
surfaceRaisedstringrequiredCards and raised controls on a surface.
textstringrequiredMain text.
textMutedstringrequiredSecondary text — captions, hints, timestamps.
borderstringrequiredHairlines and outlines.
accentstringrequiredThe brand's accent — primary buttons, the launcher, selection.
onAccentstringrequiredText and icons on the accent.
accentSoftstringrequiredThe accent at low strength — chips, selected rows, the focus halo.
successstringrequiredSuccess states.
warningstringrequiredWarnings.
dangerstringrequiredErrors, hanging up, refusal lines.
userBubblestringrequiredThe customer's message bubbles.
userBubbleTextstringrequiredText in the customer's bubbles.
agentBubblestringrequiredThe agent's message bubbles.
agentBubbleTextstringrequiredText in the agent's bubbles.
scrimstringrequiredThe dimming colour behind a sheet (drawn at reduced opacity).

ThemeShadow

Read it as one shadow of the design.

FieldTypeDefaultMeaning
ynumberrequiredHow far down the shadow falls, in points.
blurnumberrequiredHow soft it is, in points.
spreadnumberrequiredHow far it grows beyond the shape, in points.
opacitynumberrequiredHow strong it is, 0 to 1, applied to color.
colorstringrequiredThe shadow's colour.

ResolvedDesign

Read it as a whole design: the brand's choices and the tokens they resolve to in both schemes.

FieldTypeDefaultMeaning
configDesignConfigrequiredWhat the brand chose — see DesignConfig.
lightThemeTokensrequiredThe tokens for light mode.
darkThemeTokensrequiredThe tokens for dark mode.

DesignConfig

Read it as the brand's design choices, as the dashboard saves them: a preset and a handful of adjustments.

What to expect — Every field but preset is optional — absent means the preset's own answer. Unknown values are dropped, so an older SDK draws something sensible from a newer design.

FieldTypeDefaultMeaning
presetstring"graine-classic"The design philosophy — see DESIGN_PRESETS. An unknown one is the default.
accentstringthe preset'sA #RRGGBB accent replacing the preset's in both schemes.
scheme"auto" | "light" | "dark""auto"Fix the scheme, or follow the phone.
roundnessnumber10 (square) to 1 (the preset's own corners) to 2 (twice as round).
fontstringthe preset'sThe body typeface: "system" or a family from DESIGN_FONTS.
headingFontstringthe preset'sThe heading typeface, as font.
density"compact" | "comfortable" | "spacious""comfortable"How much room around things.
shadow"preset" | "none" | "soft" | "medium" | "strong" | "hard""preset"The shadows.
motion"preset" | "none" | "subtle" | "lively""preset"The transitions.
neutral"preset" | "cool" | "neutral" | "warm""preset"The tint of the greys.
glassbooleanthe preset'sTranslucent, blurred surfaces.
bubblesDesignBubblesthe preset'sHow message bubbles are drawn.
launcherIconstringthe preset'sA glyph id from LAUNCHER_ICONS, or "logo" for the brand's logo.
launcherShapeLauncherShapethe preset'sThe launcher's shape.
iconStyleIconStylethe preset'sHow the SDK's icons are drawn.
chromeDesignChromethe preset'sFrame the chat, the caption and components as desktop windows.
chatDesignChatConfignoneThe chat window's choices: title, subtitle, avatar ("logo", "glyph", "none"), placeholder, timestamps, background ("plain", "tint", "pattern"), up to four conversation starters, header ("surface", "accent"), size, and showUpcomingCalls, upcomingTitle, showHistory, newConversation.
callDesignCallConfignoneThe call screen's choices: background, avatar and its shape, captions, timer, the in-call controls, the end button's shape, the ringtone and the status words.

DesignPreset

Read it as one design philosophy — a complete look before any adjustment.

FieldTypeDefaultMeaning
idstringrequiredThe preset's id, as DesignConfig.preset names it.
namestringrequiredIts name in the dashboard.
taglinestringrequiredA one-line description, as the dashboard shows it.
family"graine" | "native" | "modern" | "enterprise" | "expressive"requiredThe group it is listed under.
inspiredBystring[]requiredThe well-known looks it draws on.
fontThemeTokens["font"]requiredIts type.
shapeThemeTokens["shape"]requiredIts corners, hairline and launcher shape.
spacingThemeTokens["spacing"]requiredIts spacing.
elevationThemeTokens["elevation"]requiredIts shadows.
motionThemeTokens["motion"]requiredIts transitions.
icons{ style: IconStyle; stroke: number; launcher: string }requiredIts icon style, stroke and launcher glyph id.
glassThemeTokens["glass"]requiredIts translucency, or null.
bubblesDesignBubblesrequiredIts message bubbles.
chromeDesignChrome"none"Its framing.
colors{ light: Omit<ThemeColors, "accentSoft">; dark: Omit<ThemeColors, "accentSoft"> }requiredIts colours for light and dark (every ThemeColors field but accentSoft, which is derived from the accent).

DESIGN_PRESETS

Use it to list the design philosophies a brand can choose.

const DESIGN_PRESETS: ReadonlyArray<DesignPreset>;

What to expect — 23 presets, in the dashboard's order, by id: graine-classic, signature, chat-neutral, material-3, ios-native, minimal-mono, fluent-2, carbon, fintech-clear, liquid-glass, soft-playful, desktop-buddy, swiss-grid, precision, japandi-calm, neo-brutalism, editorial, luxury, retro-terminal, warm-earthy, bauhaus, gamified, high-contrast. A newer server may know more; the SDK draws an unknown one as the default.

DEFAULT_DESIGN_PRESET

Read it as the design an agent that never chose one is drawn in.

const DEFAULT_DESIGN_PRESET = "graine-classic";

What to expect — "graine-classic".

designPreset

Use it to look up a preset by id.

function designPreset(id: unknown): DesignPreset
ParameterTypeDefaultMeaning
idunknownrequiredThe preset's id (any case); a former id is mapped to its current one.

Returns — the DesignPreset, or the default (graine-classic) for an id it does not know. Example value: { id: "signature", name: "Signature", … }.

Errors — none.

designPreset("signature").name; // "Signature"

normaliseDesignConfig

Use it to check a design config that came from anywhere: known keys, allowed values, nothing else.

function normaliseDesignConfig(raw: unknown): DesignConfig
ParameterTypeDefaultMeaning
rawunknownrequiredThe config as it arrived.

Returns — a DesignConfig: an unknown preset becomes the default, colours become #RRGGBB, roundness is clamped to 0–2, values outside their lists are dropped. Example value: { preset: "graine-classic", accent: "#3740B8" }.

Errors — none.

normaliseDesignConfig({ preset: "unknown", accent: "3740b8" }); // { preset: "graine-classic", accent: "#3740B8" }

designFromAppearance

Use it to read the design an agent's appearance describes — the chosen design, or the older appearance settings as one.

function designFromAppearance(appearance: unknown): DesignConfig
ParameterTypeDefaultMeaning
appearanceunknownrequiredThe agent's appearance (useGraineAgent().appearance).

Returns — appearance.design when the brand chose one; otherwise Graine Classic with the older settings' accent, corners and light or dark. Example value: { preset: "graine-classic", accent: "#318CE7", scheme: "auto", roundness: 1 }.

Errors — none.

const { appearance } = useGraineAgent();
const config = designFromAppearance(appearance);

resolveTokens

Use it to work out the tokens of one scheme from a design config.

function resolveTokens(configInput: DesignConfig, scheme: "light" | "dark"): ThemeTokens
ParameterTypeDefaultMeaning
configInputDesignConfigrequiredThe design (checked again here).
scheme"light" | "dark"requiredThe scheme to resolve.

Returns — the ThemeTokens.

Errors — none.

const dark = resolveTokens({ preset: "signature" }, "dark");

resolveDesign

Use it to work out a whole design — both schemes at once — from a config.

function resolveDesign(configInput: unknown): ResolvedDesign
ParameterTypeDefaultMeaning
configInputunknownrequiredThe design config, as it arrived.

Returns — a ResolvedDesign.

Errors — none.

const design = resolveDesign({ preset: "material-3", accent: "#0B6E4F" });

pickTokens

Use it to pick the scheme to draw: the brand's fixed one, else the phone's.

function pickTokens(design: ResolvedDesign, deviceScheme: "light" | "dark" | null | undefined): ThemeTokens
ParameterTypeDefaultMeaning
designResolvedDesignrequiredThe design.
deviceScheme"light" | "dark" | null | undefinedrequiredThe phone's scheme — useColorScheme().

Returns — design.light or design.dark: the brand's scheme when it fixed one; otherwise dark only when the phone is dark.

Errors — none.

const { design } = useGraineAgent();
const tokens = pickTokens(design, useColorScheme());

sessionDesign

Use it to get the design to draw from the agent's settings — always a complete one, even before they load.

function sessionDesign(session: { design?: unknown; appearance?: unknown } | null | undefined): ResolvedDesign
ParameterTypeDefaultMeaning
session{ design?: unknown; appearance?: unknown } | null | undefinedrequiredThe agent's settings — client.config — or null before they load.

Returns — Graine's design with any missing or malformed token filled, or — when the settings carry none — the design the appearance implies; the default design for null.

Errors — none.

const design = sessionDesign(client.config);

contrastRatio

Use it to check that a text colour reads on a background (WCAG contrast).

function contrastRatio(a: string, b: string): number
ParameterTypeDefaultMeaning
astringrequiredOne colour, #RGB or #RRGGBB.
bstringrequiredThe other colour.

Returns — the contrast ratio, from 1 (none) to 21 (black on white); 4.5 is the usual minimum for body text. Example value: 4.83.

Errors — none.

contrastRatio("#3740B8", "#FFFFFF");

readableOn

Use it to choose the text colour that reads best on a background.

function readableOn(bg: string, candidates: ReadonlyArray<string> = ["#FFFFFF", "#101318"]): string
ParameterTypeDefaultMeaning
bgstringrequiredThe background colour.
candidatesReadonlyArray<string>["#FFFFFF", "#101318"]The colours to choose from.

Returns — the candidate with the highest contrast on bg. Example value: "#FFFFFF".

Errors — none.

const theme = useGraineTheme();
readableOn(theme.colors.accent);

DesignBubbles

Read it as how message bubbles are drawn.

type DesignBubbles = "filled" | "outline" | "minimal";
ValueMeaning
"filled"Solid bubbles.
"outline"Outlined bubbles.
"minimal"No bubble — text on the page.

DesignChrome

Read it as how a surface holding content is framed.

type DesignChrome = "none" | "window";
ValueMeaning
"none"A plain surface.
"window"A desktop window, with three dots and a title bar — the playful-desktop look.

DESIGN_CHROMES

Use it to list the framings.

const DESIGN_CHROMES: ReadonlyArray<DesignChrome> = ["none", "window"];

What to expect — A fixed list.

LauncherShape

Read it as the launcher's shape.

type LauncherShape = "circle" | "squircle" | "rounded-square" | "square" | "pill";
ValueMeaning
"circle"Round.
"squircle"A rounded square with continuous curves.
"rounded-square"A square with rounded corners.
"square"Square corners.
"pill"A wide pill.

MotionEasing

Read it as how the design's transitions ease.

type MotionEasing = "standard" | "emphasized" | "spring" | "linear" | "none";
ValueMeaning
"standard"An ordinary ease.
"emphasized"A fast start and a long, soft settle.
"spring"An overshoot that settles, scaled by the token's bounce.
"linear"An even pace.
"none"No animation.

IconStyle

Read it as how the SDK's icons are drawn.

type IconStyle = "outline" | "filled" | "duotone" | "sharp";
ValueMeaning
"outline"Stroked, with round ends and joins.
"filled"The shape filled, lightly stroked.
"duotone"The shape filled at low strength, then stroked.
"sharp"Stroked, with square ends and mitred joins.

SdkIconName

Read it as the names of the SDK's own icons, drawn the same on every surface.

type SdkIconName = "mic" | "micOff" | "end" | "send" | "chat" | "call" | "chevron" | "close" | "compose";
ValueMeaning
"mic"The microphone.
"micOff"The microphone, muted.
"end"Hang up.
"send"Send a message.
"chat"The chat.
"call"A call — the booking sheet and the booked-calls card use it.
"chevron"Back, or collapse.
"close"Close.
"compose"New conversation.

SDK_ICONS

Use it to draw the SDK's icons yourself, as 24×24 paths, in the design's icon style.

const SDK_ICONS: Readonly<Record<SdkIconName | "speaker", { body?: string; lines?: string }>>;

What to expect — Each icon is a closed body that may be filled and lines that are only ever stroked, both SVG path data on a 24×24 grid. Draw them in icons.style at icons.stroke.

KeyValueMeaning
mic{ body, lines }The microphone.
micOff{ body, lines }The microphone with a strike.
end{ body }The hang-up handset.
send{ body, lines }The send arrow.
chat{ body }The chat bubble.
call{ lines }The phone handset.
chevron{ lines }The chevron.
close{ lines }The cross.
compose{ lines }A page and a pen — "New conversation".
speaker{ body, lines }The loudspeaker, for a call screen.
const t = useGraineTheme();
<Svg width={24} height={24} viewBox="0 0 24 24"><Path d={SDK_ICONS.call.lines} stroke={t.colors.accent} strokeWidth={t.icons.stroke} fill="none" /></Svg>

LAUNCHER_ICONS

Use it to list the launcher glyphs a brand can choose, as 24×24 SVG paths.

const LAUNCHER_ICONS: ReadonlyArray<{ id: string; label: string; path: string; stroke: boolean }>;

What to expect — 17 glyphs: waveform, audio-lines, sparkles, microphone, message-circle, headset, bot, phone, zap, star, orbit, sparkle-filled, buddy, chat-dots-filled, graphic-eq, grain, grain-voice. Each has an id, a label for the dashboard, its path, and stroke — true to stroke it at icons.stroke, false to fill it.

glyphUrl

Use it to fetch a launcher glyph as a PNG image instead of drawing the path.

function glyphUrl(origin: string, id: string, color: string, sizePx: number, stroke = 2): string
ParameterTypeDefaultMeaning
originstringrequiredGraine's address — client.opts.baseUrl.
idstringrequiredThe glyph's id from LAUNCHER_ICONS.
colorstringrequired#RRGGBB (white when it cannot be read).
sizePxnumberrequiredThe size in physical pixels (points × screen scale), so it is sharp — 16 to 512.
strokenumber2The stroke width, 0.5 to 4.

Returns — the image's address on Graine. Example value: "https://www.graine.ai/api/embed/glyph?id=waveform&c=FFFFFF&s=168&w=2&v=2".

Errors — none.

<Image source={{ uri: glyphUrl(client.opts.baseUrl, "waveform", "#FFFFFF", 56 * PixelRatio.get()) }} style={{ width: 56, height: 56 }} />

LogoPlate

Read it as what the brand's logo sits on, chosen in Appearance → Logo.

type LogoPlate = "accent" | "white";
ValueMeaning
"accent"The shape's own accent fill — for a white or light logo made for a coloured background. The default.
"white"A white plate with a hairline — for a coloured or dark logo, or a logo on an opaque image.

LOGO_PLATES

Use it to list the logo plates.

const LOGO_PLATES: ReadonlyArray<LogoPlate> = ["accent", "white"];

What to expect — A fixed list.

LOGO_PLATE_WHITE

Read it as the white plate's colour.

const LOGO_PLATE_WHITE = "#FFFFFF";

What to expect — "#FFFFFF".

normaliseLogoPlate

Use it to read a logo plate setting safely.

function normaliseLogoPlate(value: unknown): LogoPlate
ParameterTypeDefaultMeaning
valueunknownrequiredThe setting as it arrived.

Returns — "white" for "white", else "accent". Example value: "accent".

Errors — none.

const { appearance } = useGraineAgent();
normaliseLogoPlate(appearance?.logoPlate);

logoBox

Use it to size the brand's logo inside the launcher or an avatar — whole, centred, never cropped.

function logoBox(shape: string, size: number): number
ParameterTypeDefaultMeaning
shapestringrequiredThe shape's LauncherShape.
sizenumberrequiredThe shape's size across (a pill: its height), in points.

Returns — the side of the square the logo fits in: 80% of size for a square or rounded square, 70% for a circle, squircle or pill, rounded down, at least 1. Example value: 39 for a 56-point circle.

Errors — none.

const box = logoBox("circle", 56);
<Image source={{ uri: logoUrl }} resizeMode="contain" style={{ width: box, height: box }} />

logoPlateLook

Use it to draw what the logo sits on: the fill, and the outline when there is one.

function logoPlateLook(plate: LogoPlate, colors: Pick<ThemeColors, "accent" | "border">, borderWidth = 1): { fill: string; outline: { width: number; color: string } | null }
ParameterTypeDefaultMeaning
plateLogoPlaterequiredThe plate.
colorsPick<ThemeColors, "accent" | "border">requiredThe design's accent and border colours.
borderWidthnumber1The outline's width, at least 1.

Returns — for "white": { fill: "#FFFFFF", outline: { width, color: colors.border } }; for "accent": { fill: colors.accent, outline: null }. Example value: { fill: "#3740B8", outline: null }.

Errors — none.

const theme = useGraineTheme();
const plate = logoPlateLook("white", theme.colors);

DESIGN_FONTS

Use it to list the typefaces the dashboard offers, with the scripts each covers.

const DESIGN_FONTS: ReadonlyArray<{ family: string; category: "sans" | "serif" | "mono" | "display"; scripts: string[] }>;

What to expect — 25 entries: system (the phone's own font, every script it covers), and families such as Inter, Roboto, Geist, IBM Plex Sans, DM Sans, Noto Sans, Mukta, Hind, Poppins, Nunito, Atkinson Hyperlegible Next, Source Serif 4, Newsreader, Fraunces, Playfair Display, Martel and JetBrains Mono. category is "sans", "serif", "mono" or "display"; scripts lists what each can write (latin, devanagari, tamil, …) — a Latin-only face needs a partner for other scripts.

FontLoader

Pass expo-font's Font.loadAsync as the provider's loadFont: it is the shape the SDK needs to register the design's typefaces.

type FontLoader = (fonts: Record<string, string>) => Promise<unknown>;

What to expect — Called with { "Graine-DMSans-700": "https://…" }-style maps — the registered name and the file's address on Graine; a face that fails to load is drawn in the system font.

FontFace

Read it as one face of one family, as the SDK registers it.

FieldTypeDefaultMeaning
familystringrequiredThe family, as the design names it.
weightnumberrequiredThe weight this face draws (the nearest the family has).
namestringrequiredThe fontFamily it is registered under — "Graine-DMSans-700".
urlstringrequiredWhere Graine serves the file.

faceFor

Use it to find the face to load for a family at a weight.

function faceFor(family: string | null | undefined, weight: number, origin = "https://www.graine.ai"): FontFace | null
ParameterTypeDefaultMeaning
familystring | null | undefinedrequiredThe family (any case).
weightnumberrequiredThe weight wanted.
originstring"https://www.graine.ai"Graine's address.

Returns — the FontFace at the nearest weight the family has (ties go heavier), or null for "system" or a family without files. Example value: { family: "DM Sans", weight: 700, name: "Graine-DMSans-700", url: "https://www.graine.ai/api/embed/font?f=DM%20Sans&w=700&v=1" }.

Errors — none.

faceFor("DM Sans", 650);

facesForDesign

Use it to list every face a design draws with, in either scheme — to load them ahead of time.

function facesForDesign(design: ResolvedDesign, origin?: string): FontFace[]
ParameterTypeDefaultMeaning
designResolvedDesignrequiredThe design.
originstring"https://www.graine.ai"Graine's address.

Returns — the body family at its body and strong weights and the heading family at its title and strong weights, for both schemes, each once.

Errors — none.

const { design } = useGraineAgent();
const faces = facesForDesign(design, client.opts.baseUrl);
await Font.loadAsync(Object.fromEntries(faces.map((f) => [f.name, f.url])));

fontUrl

Use it to build the address of one typeface file on Graine.

function fontUrl(origin: string, family: string, weight: number): string
ParameterTypeDefaultMeaning
originstringrequiredGraine's address.
familystringrequiredThe family.
weightnumberrequiredThe weight.

Returns — the file's address, including the file version. Example value: "https://www.graine.ai/api/embed/font?f=Inter&w=400&v=1".

Errors — none.

fontUrl(client.opts.baseUrl, "Inter", 400);

FONT_ASSET_VERSION

Read it as the version of the typeface files, part of their address so no cache keeps an old file.

const FONT_ASSET_VERSION = 1;

What to expect — 1.

SvgKit

Pass react-native-svg's two components as the provider's svg, so the SDK can draw its icons as vectors without depending on the library.

FieldTypeDefaultMeaning
SvgReact.ComponentType<any>requiredreact-native-svg's Svg.
PathReact.ComponentType<any>requiredreact-native-svg's Path.

resolveNativeAppearance

Use it to read the agent's appearance safely for a bar you draw yourself: a valid accent, the heading, the logo's address and the corner radius.

function resolveNativeAppearance(appearance: Record<string, any> | null | undefined, fallback: NativeAppearanceFallback): NativeAppearance
ParameterTypeDefaultMeaning
appearanceRecord<string, any> | null | undefinedrequiredThe agent's appearance (useGraineAgent().appearance).
fallbackNativeAppearanceFallbackrequiredWhat to use where the dashboard has nothing — see NativeAppearanceFallback.

Returns — a NativeAppearance. Example value: { accent: "#3740B8", heading: "Acme assistant", logoUrl: "https://www.graine.ai/api/embed/logo?k=…", logoPlate: "accent", radius: 18 }.

Errors — none.

const { appearance } = useGraineAgent();
const look = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "Assistant" });

NativeAppearance

Read it as the agent's appearance, ready to draw.

FieldTypeDefaultMeaning
accentstringrequiredA valid #RGB or #RRGGBB accent.
headingstringrequiredThe panel's heading as written in the dashboard — a title, not necessarily the agent's name.
logoUrlstring | nullrequiredThe brand's logo as an absolute address, ready for an Image; null when there is none.
logoPlateLogoPlaterequiredThe backing behind the logo when the logo replaces the launcher glyph.
radiusnumberrequiredCorner rounding in points, within what your bar allows.

NativeAppearanceFallback

Read it as what to draw where the dashboard has nothing.

FieldTypeDefaultMeaning
accentstringrequiredThe accent when the dashboard's is missing or invalid.
headingstringrequiredThe heading when the dashboard has none.
radiusnumber18The rounding when the dashboard has none.
maxRadiusnumber48The most rounding your bar will draw (never above 48).
originstring"https://www.graine.ai"Graine's address, for the logo.

chatPresentation

Use it to decide how a chat window of a size is drawn on a screen this big — the rule the SDK's windows follow.

function chatPresentation(size: unknown, width: number, height: number, platform: "app" | "web" = "app"): ChatPresentation
ParameterTypeDefaultMeaning
sizeunknownrequiredThe size to open at — see ChatSize; an unknown word is compact.
widthnumberrequiredThe window's width, in points.
heightnumberrequiredThe window's height, in points.
platform"app" | "web""app"An app's window, or a web page's.

Returns — "window" for compact; for large, "sheet" on a phone and "panel" on a tablet (shorter side 600 points or more); for full, "screen" in an app (a web page on a computer gets "panel"). Example value: "sheet".

Errors — none.

const { chatSize } = useGraineAgent();
const { width, height } = useWindowDimensions();
const how = chatPresentation(chatSize, width, height);

ChatPresentation

Read it as where a chat window of a size goes.

type ChatPresentation = "window" | "sheet" | "panel" | "screen";
ValueMeaning
"window"Above the voice bar.
"sheet"Up from the bottom over about 92% of a phone's screen.
"panel"A panel 420 points wide in the bottom corner of a tablet (or a computer).
"screen"The whole screen.

chatPanelSize

Use it to size a chat panel on a screen this tall.

function chatPanelSize(height: number): { width: number; height: number }
ParameterTypeDefaultMeaning
heightnumberrequiredThe screen's height, in points.

Returns — width 420; height 90% of the screen, at most 760. Example value: { width: 420, height: 760 }.

Errors — none.

chatPanelSize(1024); // { width: 420, height: 760 }

CHAT_WINDOW

Read it as the numbers a large or full chat window is drawn with.

const CHAT_WINDOW = { sheetTop: 0.08, panelWidth: 420, panelMaxHeight: 760, panelHeight: 0.9, tabletMinSide: 600, webPhoneBelow: 640 } as const;
KeyValueMeaning
sheetTop0.08How far below the top safe area a sheet's top edge starts, as a fraction of the screen's height.
panelWidth420A panel's width, in points.
panelMaxHeight760A panel's greatest height, in points.
panelHeight0.9A panel's height as a share of the screen's, up to panelMaxHeight.
tabletMinSide600An app's window counts as a tablet's when its shorter side is at least this, in points.
webPhoneBelow640A website draws a phone's sheet and full screen only while the page is narrower than this.

Going live

Use this before a release build: the versions to ship, the settings a release needs, and the checks that catch a build without the calls package.

Release checklist

Work through it once per release.

  • Versions. @graineai/inapp-react-native 0.42.0 is JavaScript only: it ships in an over-the-air update. @graineai/inapp-react-native-calls 0.3.0 carries native code — the call engine of Graine's iOS and Android SDKs 0.8.0 — so adding or upgrading it needs a new store build (pod install on iOS); read GraineCalls.isAvailable before calling it from code an over-the-air update may run on an older build.
  • The agent accepts your app. The agent's Allowed domains (Agent → Embed & Widgets) include app://; without it every request is refused with origin_refused.
  • The key. The provider and GraineCalls.configure use the agent's pk_live_… key.
  • Install id. Pass storage (AsyncStorage) to the provider, or link the calls package — memory, conversations that continue and booked calls need an install id.
  • Debug logs off. GraineCalls.configure({ debug: false }) in release builds (it is off unless you turn it on).
  • Your app's version. Pass appVersion to the provider and to GraineCalls.configure, so each conversation and call says which build it came from.
  • iOS. aps-environment is production for TestFlight and App Store builds; Push Notifications and Background Modes → Audio and Voice over IP are on; test calls on a real iPhone — CallKit and VoIP pushes do not run in the simulator.
  • Android. Ask for notifications (POST_NOTIFICATIONS) on Android 13+; Firebase in the release build (google-services.json, and Firebase Cloud Messaging — the package's Expo plugin or graineFirebaseMessaging=true brings it), so the package registers for pushes itself. With @react-native-firebase/messaging, hand every Firebase message to GraineCalls.handlePush first, from index.js, and refreshed tokens to registerPushToken.
  • Expo. A development or store build, never Expo Go.
  • WebRTC calls, if you ship them. ENV["GRAINE_WEBRTC"] = "1" in ios/Podfile and graineWebRtc=true in android/gradle.properties — or GRAINE_WEBRTC=1 in the build server's environment — in the release build itself; list WebRTC (BSD 3-Clause) in your app's open-source licences. The agent's In-app call audio setting can move every install back to the WebSocket without a release. See WebRTC calls.

GraineCalls.isAvailable

Read it to know whether this build carries the calls package's native code — before you show a "Call" button from code an over-the-air update may run on an older build.

get isAvailable(): boolean

Returns — true when the native module is linked into this build. Example value: true.

Errors — none: it is the one member that never throws without the native code.

{GraineCalls.isAvailable && <Button title="Call us" onPress={() => GraineCalls.startCall()} />}

GraineInAppClient.setAppVersion

Call it with your app's version so each conversation's record says which build it happened in — the provider does it from its appVersion prop.

setAppVersion(version: string | null | undefined): void
ParameterTypeDefaultMeaning
versionstring | null | undefinedrequiredYour app's version — "4.2.1 (310)"; empty or null clears it.

Returns — nothing. It travels with the screen context from then on.

Errors — none.

import DeviceInfo from "react-native-device-info";
 
client.setAppVersion(DeviceInfo.getReadableVersion());

Events and listeners

Use these for what you do once, when something happens — log it, count it, show a toast. For drawing, read state from the hooks instead.

useGraineEvents

Call it in a component to hear the conversation's events as they happen, in order; it unsubscribes on unmount.

function useGraineEvents(handler: (e: GraineEvent) => void): void
ParameterTypeDefaultMeaning
handler(e: GraineEvent) => voidrequiredCalled with each event — see GraineEvent. An inline function is fine: it is not re-subscribed on every render.

Returns — nothing.

Errors — throws outside a GraineProvider.

What to expect — Events fire once, at the moment, on the JavaScript thread; nothing is replayed to a handler that subscribes later.

useGraineEvents((e) => {
  if (e.type === "conversation_ended") analytics.log("agent_ended", { reason: e.reason });
  if (e.type === "friction_detected") badgeHelpButton();
});

GraineEvent

Read it to react to the conversation once, when something happens; branch on type.

type GraineEvent =
  | { type: "conversation_started" } | { type: "conversation_ended"; reason: string }
  | { type: "agent_speaking"; speaking: boolean } | { type: "agent_volunteered"; text: string }
  | { type: "action_requested"; name: string } | { type: "action_completed"; name: string; status: string; message?: string }
  | { type: "muted"; muted: boolean } | { type: "error"; message: string } | { type: "mic_denied"; reason: string }
  | { type: "launcher_shown"; screen: string | null } | { type: "friction_detected"; name: string; screen: string; detail: string }
  | { type: "connection_quality"; quality: ConnectionQuality; rtt?: number; jitter?: number; loss?: number }
  | { type: "network"; online: boolean } | { type: "reconnecting"; attempt: number; delayMs: number };
typeFieldsWhen it fires, and what to do with it
conversation_startednoneThe provider's chat connection opened — count conversations.
conversation_endedreasonIt closed: reason tells the customer hanging up (or your app closing it) from a connection that died — keep them apart in your funnel.
agent_speakingspeakingThe agent started (true) or stopped (false) speaking.
agent_volunteeredtextThe agent spoke first — a turn nobody asked for — with its first words; worth measuring on its own. The provider also opens its panel.
action_requestednameThe agent asked your app to run one of your actions.
action_completedname, status, messageYour app answered it: status ok, refused or error, and the handler's own message when it gave one — count failures.
mutedmutedThe customer was muted or unmuted on the provider's client.
errormessageLoud for you, invisible to the customer: the agent could not be started, the chat could not open, or the voice launcher reported something to fix — an engine page that will not load, a transport the agent has no setup for, a call refused while another was live. message is the sentence useGraineAgent().error or the launcher's onError also gets — log it.
mic_deniedreasonThe microphone was refused ("NotAllowedError") or unusable ("NotFoundError") — a call that could have happened and did not; a WebRTC call of your own (RtcVoiceSessionController with client) reports it too.
launcher_shownscreenThe launcher became visible on a screen (the route's name, or null) — the moment a nudge is seen.
friction_detectedname, screen, detailThe customer looks stuck — a FrictionKind, the screen, and the sentence the agent was told. Open your launcher or badge a control.
connection_qualityquality, rtt, jitter, lossThe call's connection changed word (ConnectionQuality), with the measurement when there is one. Fires only on a change.
networkonlineThe device went offline (false) or came back (true).
reconnectingattempt, delayMsThe chat dropped; try number attempt comes in delayMs milliseconds.

GraineInAppClient.on

Call it outside React to hear one of the client's events; it returns the function that stops listening.

on(event: string, fn: Listener): () => void
ParameterTypeDefaultMeaning
eventstringrequiredThe event's name — see the table below.
fn(payload: any) => voidrequiredCalled with the event's payload. A listener that throws is logged and never stops the others.

Returns — a function that removes the listener.

Errors — none.

What to expect — Events fire as they happen; nothing is replayed to a listener added later (the last component sent is kept as pendingWidget).

EventPayloadWhen it fires
sessionSessionConfigThe agent's settings loaded.
identityIdentityChannelWho the customer is, this install, the memory choice or the time zone changed.
variablesRecord<string, string>The merged prompt variables changed (setVariables, identify).
screen{ screen: ScreenContext | null; actions: string[] }The screen or the registered actions changed — at once, for a host carrying its own conversation.
open{ transport: "gateway" | "direct" }This client's connection opened.
close{ code?: number; reason?: string; byClient?: true }It closed: byClient when your app (or the SDK) closed it; reason "another_agent" when another agent took the line.
reconnecting{ attempt: number; delayMs: number }A dropped chat tries again in delayMs.
chunkstringWords of the agent's reply.
turn_startnoneAn agent turn begins.
turn_endnoneThe agent's words for this turn are complete.
agent_speakingbooleanThe agent started or stopped speaking.
caption{ role: string; text: string; live: boolean }A caption line from the voice launcher's call.
transcript{ text: string; final: boolean }The customer's speech, recognised, on this client's spoken connection.
text_sent{ id: string; text: string }A message that was waiting went out.
relay_text{ id: string; text: string }For the voice launcher: a typed message to hand to the call it holds.
historyRestoredTurn[]An earlier conversation was restored.
chat_cleared{ reason: "memory_forgotten" | "signed_out" | "another_user" | "new_conversation" | "app" }The chat on screen should empty — "app" when your app called clearChat().
chat_failedbooleanThe chat gave up (true) — its first connect failed, or Graine ended or refused the conversation — or someone asked again or it opened (false). See chatFailed.
widgetthe component as sentThe agent drew a component (widget_id, kind, data, fallback_text, style, on_select).
widget_pending{ widget_id?: string; kind?: string }A component is on its way.
widget_result{ widget_id: string; data: Record<string, unknown>; summary: string }Your app answered a component.
actionAppActionRequestThe agent asked for one of your actions; your handler runs next.
action_result{ name: string; status: string; message?: string }An action finished, or was refused.
app_eventAppEventreportEvent (or a friction signal) told the agent something.
frictionFrictionSignalThe customer looks stuck.
highlight{ name: string | null; ttlMs: number }Point at an element (or stop).
mutedbooleanMuted or unmuted.
quality{ quality: ConnectionQuality; rtt?: number; jitter?: number; loss?: number }The connection's word changed.
network{ online: boolean }The device went offline or came back.
mic_denied{ reason: string }The microphone was refused — for the voice launcher's call, or a WebRTC call of your own started with client.
voice_error{ message: string }The voice launcher reported something to fix, as its onError does; useGraineEvents hears it as error.
call_state{ connected: boolean; connecting: boolean }The voice launcher's call is connecting, connected, or over.
audio{ data: string; format: string | null; sampleRate: number | null }A frame of the agent's voice on this client's spoken connection (base64).
clearnoneThe customer talked over the agent: drop the agent's queued audio.
memories_changed{ forgotten: true; deleted: number }forgetMemory() deleted the customer's memories.
scheduled_calls_changed{ booked: string } | { cancelled: string }A call was booked or cancelled through the SDK.
open_chat{ size: ChatSize | null }openChat() asked for the chat.
open_booking{ id: number; reason: string | null }openBooking() asked for the booking sheet.
surface{ surface: AgentSurface }The customer switched from the call to the chat, or back.
voice_host{ present: boolean }A voice launcher mounted or unmounted.
native_call{ live: boolean }A call from the calls package went live, or ended.
line_taken{ reason: "another_agent" }Another agent took the line from this client's call.
diagnostic{ type: "actions"; usable: string[]; undeclared: string[]; unhandled: string[] }Your registered actions and the agent's declared ones do not match.
framethe message as sentGraine sent something this release does not handle — for diagnostics.
const off = client.on("scheduled_calls_changed", () => refreshMyList());

GraineInAppClient.emit

The SDK's own views call it to pass events between the pieces that share one client; your app does not need it.

emit(event: string, payload?: any): void
ParameterTypeDefaultMeaning
eventstringrequiredThe event's name.
payloadanynoneWhat it carries.

Returns — nothing. Every listener of event is called; one that throws is logged and the others still run.

Errors — none.

client.emit("mic_denied", { reason: "NotAllowedError" });

Errors

Use these to handle what can go wrong: the error a request to Graine rejects with and every code it carries, what stops voice from starting, the WebRTC controller's errors, the calls package's, and the limits that apply.

GraineError

Catch it from any method that asks Graine on the customer's behalf — memory, booking, a new conversation, the agent's settings: branch on code, and show message, which is written for the customer.

class GraineError extends Error {
  readonly code: GraineErrorCode;
  readonly status: number;
  readonly retryAfterSeconds: number | null;
  constructor(code: GraineErrorCode, message: string, status = 0, retryAfterSeconds: number | null = null)
}
MemberTypeMeaning
constructor(code, message, status = 0, retryAfterSeconds = null)Builds one — for tests, or to reject the same way from your own wrappers.
codeGraineErrorCodeWhat went wrong, as a code to branch on — see GraineErrorCode.
statusnumberThe HTTP status of Graine's answer; 0 when the SDK refused on the device or no answer came back.
retryAfterSecondsnumber | nullWith rate_limited: how long Graine asked you to wait, in seconds, when it said; else null.
messagestringA sentence fit for the customer. It never carries the identifier, traits or a push token.
namestringAlways "GraineError".
try {
  await client.forgetMemory();
} catch (e) {
  if (e instanceof GraineError) {
    if (e.code === "rate_limited" && e.retryAfterSeconds) retryIn(e.retryAfterSeconds);
    else showMessage(e.message);
  }
}

GraineErrorCode

Read it to know every code a GraineError can carry, when it happens and how to fix it.

type GraineErrorCode = "invalid_key" | "origin_refused" | "rate_limited" | "invalid_request" | "identity_required" | "device_limit" | "scheduling_off" | "too_soon" | "too_far" | "outside_window" | "too_many_pending" | "not_found" | "unavailable" | "network" | "unknown" | (string & {});

What to expect — A code newer than this SDK arrives as its own word: give every switch a default.

CodeHTTPMeansRaised byFix
invalid_key401The publishable key is not one, or belongs to no agent.Every request to GraineCopy the key again from Agent → Embed & Widgets → Install.
origin_refused403The app is not allowed: app:// is missing from the agent's Allowed domains.Every requestAdd app:// to the agent's Allowed domains.
rate_limited429Too many requests from this install, key or address.Every requestWait retryAfterSeconds when Graine said how long, then retry.
invalid_request400 or 422 (0 on the device)A field is missing or malformed — a time that cannot be read, an empty call id, no install id.scheduleCall, scheduledCalls, cancelScheduledCall, memories, forgetMemory, newConversationFix the request; for the install id, pass storage or link the calls package.
identity_required400 (0 on the device)No customer is identified, or the identifier is a placeholder.memories, forgetMemory, scheduleCallCall identify() with the customer's identifier first.
device_limit403This install is not one of the customer's allowed devices.memories, forgetMemory, newConversation, scheduleCallSign out on another device, raise the agent's Devices per user, or Reset devices for the customer.
scheduling_off403 (0 on the device)The agent does not take bookings from the app.scheduleCallTurn on booking for the agent, or hide booking when bookingAvailable is false.
too_soon400 (0 on the device)Earlier than schedulingRules.earliest(now).scheduleCallOffer times from earliest(now).
too_far400 (0 on the device)Later than schedulingRules.latest(now).scheduleCallOffer times up to latest(now).
outside_window400Outside the agent's calling hours, in the customer's time zone.scheduleCallShow the message — it names a time that works — and offer another.
too_many_pending409The customer already has as many booked calls waiting as the agent allows.scheduleCallCancel one first.
not_found404No such booked call for this customer or install.cancelScheduledCallRefresh the list with scheduledCalls().
unavailable503 (504 on a timeout)Graine could not answer just now — including a booking refused because the agent is switched off or the workspace is out of credit.Every requestRetry with backoff; check the agent is on and the workspace has credit.
network— (0)No answer came back: offline, or the connection dropped. "Could not reach Graine. Check the connection and try again."Every requestRetry when online.
unknownotherAn answer the SDK could not read, or a booking whose details did not come back.Every requestLog the code and message; show the message.

When voice cannot start

Read this when the voice bar will not start: the reason reaches onError and api.error (with onTransport for a WebRTC fallback), not a GraineError.

ReasonHTTPMeansFix
key_missing401No key reached Graine.Pass publishableKey to the provider.
key_malformed401Not a publishable key.Use the pk_live_… key from Agent → Embed & Widgets.
key_unknown401No agent has that key.Copy the key again.
no_origin403The app is not allowed — the usual one for apps.Add the literal entry app:// to the agent's Allowed domains.
no_domains_configured403The agent's Allowed domains list is empty, which allows nothing.Add app://.
origin_not_allowed403This origin is not on the list.Add it to Allowed domains.
not_configured503WebRTC is not enabled for your workspace. The call still works the other way.Ask Graine support; it cannot be fixed from the app.

A refused microphone arrives as onMicDenied("NotAllowedError") ("NotFoundError" for a phone with no usable microphone). An engine that could not load after every retry says "The assistant could not load. Check your connection and try again."; a start while a call from the calls package holds the line says "A call is already in progress." Everything that reaches onError also reaches useGraineEvents as an error event, so analytics can hear it without wiring onError.

RtcVoiceError

Catch it from RtcVoiceSessionController.start(): branch on code, show message.

class RtcVoiceError extends Error {
  readonly code: RtcVoiceErrorCode;
  readonly cause?: unknown;
  constructor(code: RtcVoiceErrorCode, message: string, cause?: unknown)
}
MemberTypeMeaning
constructor(code, message, cause?)Builds one — for tests.
codeRtcVoiceErrorCodeWhat went wrong — see RtcVoiceErrorCode.
causeunknownThe underlying error, when there was one — for your logs.
messagestringWhat went wrong, in a sentence.
const call = new RtcVoiceSessionController({ publishableKey: "pk_live_…", client });
try { await call.start(); } catch (e) { if (e instanceof RtcVoiceError && e.code === "mint_failed") showSetupHelp(e.message); }

RtcVoiceErrorCode

Read it to know why a WebRTC call could not start.

type RtcVoiceErrorCode = "mint_failed" | "microphone_denied" | "connect_failed" | "already_active";
ValueMeaning
"mint_failed"The call could not be set up: no publishableKey ("Pass publishableKey (or getSession for a custom backend)."), Graine unreachable ("Could not reach the call service."), or refused — the message is Graine's own, or "Could not start a call (status)."
"microphone_denied"The customer refused the microphone — "The microphone was not allowed." They can allow it in Settings; trying again will not help until they do. With opts.client set, the client's events hear mic_denied too.
"connect_failed"The connection or the call failed after it was set up.
"already_active"A call is already starting or live on this controller — "A call is already in progress."

Calls package errors

Read this for what the calls package throws or rejects with — plain Errors with these messages, or the engine's invalid_options code.

WhereMessageFix
Any method that needs the native code"@graineai/inapp-react-native-calls is not linked: rebuild the app (pod install on iOS) — it carries native code, so an over-the-air update cannot add it."Ship a build with the package; check GraineCalls.isAvailable.
configure"publishableKey is required", "displayName is required", publishableKey is required for the agent "sales", "An agent's name must be a non-empty string.", "default" is the agent with the top-level publishableKey; name the othersFix the options; the last good configuration stays in force.
configurecode invalid_options, a message naming the optionFix that option's value.
startCallNo agent named "sales" is configured. Call GraineCalls.configure("sales", { publishableKey, … }) first.Configure the agent first.
registerPushToken"registerPushToken needs the token", or "…cannot register push tokens in this build: rebuild the app…"Pass the token; rebuild with a current package.

A call that fails reports through onOutcome and the snapshot's error, not by throwing — see GraineCallOutcome.

Rate limits and quotas

Read this to know the limits that apply to every app, and what the agent's settings let you change.

LimitValueWhere it applies
Bookings from one install10 an hourscheduleCall, and the agent's bookings in that install's conversations
Bookings in a workspace1,000 an hourAll in-app bookings
Calls waiting per customer3 by default (1–10)At most this many upcoming calls per user
Devices per customer2 by default (1–5)Devices per user; idle devices free their place after 30 days
Rings per customer per day3 by default (1–10)Ring each user at most, in the customer's own day
Quiet period after a decline6 hoursNo ring for that customer
Notice and horizon5 minutes (2–1440) · 14 days (1–30)With at least this much notice · Booked at most this far ahead
Ring time · ring again45 s (20–60) · once after 10 min (0–120)Ring for · After a missed call, ring once more after
Lateness10 minutesA booked call more than 10 minutes late is not rung
Booking reason · variables120 characters · 12 variables, 1 KB in allBookings, and the variables a conversation's ticket carries
Memories listedAt most 100, newest firstmemories()
Conversation kept50 messages, 7 days after the lastConversations that continue
Requests per key and address, per minuteStarting a conversation 30 (300 per key); voice sessions 10 (120 per key); booking, listing, cancelling, device registration, memory list and sign-out 30 (300 per key); call outcomes 60 (600 per key); forgetting memory 10 (120 per key); listing memories also 30 per installAnswered rate_limited, with how long to wait
An action's answer · a component's contentAbout 5.5 seconds · 8 secondsAfter that, the agent is told the action did not finish; a component shows what it was drawn with

Lower-level building blocks

Use these only when you carry a conversation yourself — your own connection to Graine — instead of the client: how variables travel, how the agent's text marks its turns, and how an earlier conversation arrives.

TICKET_VARIABLE_LIMITS

Read it to know how many prompt variables a conversation's ticket carries, and how long they may be — the usual connection takes your variables from the ticket.

const TICKET_VARIABLE_LIMITS = { keys: 12, keyLength: 40, valueLength: 200, totalBytes: 1024 } as const;
KeyValueMeaning
keys12At most 12 variables: who_called and call_reason first, then the rest in their order — the first 12 with a usable value are kept.
keyLength40A name is cut to 40 characters.
valueLength200A value is cut to 200 characters.
totalBytes1024The set is trimmed to fit 1 KB (names and values in UTF-8, plus a few bytes each): the biggest values are left off first, one at a time, until the rest fit — who_called and call_reason last.

What to expect — Every conversation opened by the client asks Graine for a single-use ticket carrying your variables within these limits; the agent's context comes from the ticket. A direct connection (no ticket) carries them in its address instead — see contextToken.

variablesForTicket

Use it to trim variables to what a ticket carries, exactly as the client does.

function variablesForTicket(vars: Record<string, unknown> | null | undefined): Record<string, string> | undefined
ParameterTypeDefaultMeaning
varsRecord<string, unknown> | null | undefinedrequiredThe variables — client.allVariables().

Returns — at most TICKET_VARIABLE_LIMITS.keys variables as strings — who_called and call_reason first — names and values cut to their limits; null, undefined and object values, and empty names or values, are skipped. Over 1 KB, the biggest values are left off one at a time until the rest fit (who_called and call_reason only when nothing else is left), so keep the ones that matter short. undefined when nothing is left. Example value: { first_name: "Asha", plan: "Plus" }.

Errors — none.

variablesForTicket({ first_name: "Asha", plan: "Plus", cart: { items: 3 } }); // { first_name: "Asha", plan: "Plus" }

contextToken

Use it to encode variables for a direct connection's address, the way the client does when no ticket can be had.

function contextToken(variables: Record<string, unknown> | null | undefined, callType = "webcall"): string | null
ParameterTypeDefaultMeaning
variablesRecord<string, unknown> | null | undefinedrequiredThe variables; null, undefined and object values are dropped, numbers and booleans become strings.
callTypestring"webcall"The conversation's kind: "webcall" for a spoken one, "text_chat" for a typed one.

Returns — base64 (UTF-8, so any script is safe) of the variables together with the conversation's kind — for the address's context_token parameter; null when no variable is left or they cannot be encoded. Example value: "eyJmaXJzdF9uYW1lIjoiQXNoYSIs…".

Errors — none.

const token = contextToken(client.allVariables(), "text_chat");

readAgentChunk

Use it to read one text message of the agent's reply: the words to show, and whether it opens or closes a turn.

function readAgentChunk(raw: unknown): AgentChunk
ParameterTypeDefaultMeaning
rawunknownrequiredThe text as it arrived.

Returns — an AgentChunk: the turn markers (STREAM_START, STREAM_END, in any case) taken out and nothing else touched — no trimming, since the pieces are joined into a sentence. Example value: { text: "Sure — ", startsTurn: true, endsTurn: false }.

Errors — none.

socket.onmessage = (message) => {
  const { text, startsTurn, endsTurn } = readAgentChunk(message.data);
};

AgentChunk

Read it as one piece of the agent's reply.

FieldTypeDefaultMeaning
textstringrequiredThe words to show — often empty, when the piece is only a marker.
startsTurnbooleanrequiredThis piece opened an agent turn: start a new bubble.
endsTurnbooleanrequiredThis piece closed the turn.

STREAM_START

Read it as the marker that opens an agent turn inside its text — never words to show.

const STREAM_START = "<beginning_of_stream>";

What to expect — Taken out by readAgentChunk, which matches it in any case.

STREAM_END

Read it as the marker that closes an agent turn inside its text — never words to show.

const STREAM_END = "<end_of_stream>";

What to expect — Taken out by readAgentChunk. On a spoken conversation it means the turn's text is complete; the voice may play on for some seconds.

readHistoryFrame

Use it to read the earlier conversation Graine sends when an identified customer's chat continues.

function readHistoryFrame(frame: unknown): RestoredTurn[] | null
ParameterTypeDefaultMeaning
frameunknownrequiredThe message as it arrived (its turns list).

Returns — the readable turns, oldest first — see RestoredTurn; null when the message has no turns list (then ignore it). A turn that cannot be read is skipped: one that is not an object, has a role other than the customer's or the agent's, blank text, or a time that is not ISO-8601 with a zone. Example value: [{ id: "t_81", role: "customer", text: "Where is my order?", at: 1760400000000, channel: "chat" }].

Errors — none: it never throws.

const [chat, setChat] = useState(EMPTY_CHAT);
socket.onmessage = (message) => {
  const turns = readHistoryFrame(JSON.parse(message.data));
  if (turns?.length) setChat((s) => foldChat(s, { type: "history", turns }));
};

What's new in 0.42.0 and calls 0.3.0

@graineai/inapp-react-native 0.42.0:

  • Booking with the SDK's own sheet. client.openBooking({ reason }) opens a booking sheet the provider draws in the agent's design — the day, a grid of times every 15 minutes, the phone's time zone, "What's it about? (optional)" and Book call — and resolves with the booked call, or null when the customer closes it. GraineBookingSheet is the same sheet for an app that holds its open state itself.
  • Whether to offer booking. client.bookingAvailable and client.bookingBlocker, and bookingAvailable / openBooking in useGraineAgent() and useGraineScheduling(): true only when the agent lets customers book, Graine can ring this phone, and the customer is identified.
  • Refusals in plain words. A refused booking stays in the sheet as one line — "Pick a time at least 5 minutes from now", "Sign in to book a call", … — worded from the agent's own numbers.
  • Booked calls and "Book a call" in the chat. The SDK's chat windows (GraineVoiceBar's and GraineChatScreen) now show the customer's booked calls — an Upcoming calls card, as the design allows, each call with Cancel — and Book a call under them, or in their place when none are booked, wherever booking is available. chatPieces takes bookingAvailable and returns book.
  • The booking rules, for your own screen. bookingBounds, bookingDays, bookingSlots, clampBooking, bookingDayLabel, bookingCallName, bookingSubtitle, bookingZoneLine, bookingDoneLine, bookingErrorLine, upcomingCalls, upcomingTimeLabel, BOOKING_TEXT and the BOOKING_* numbers — the same rules and words as every platform's sheet.

@graineai/inapp-react-native-calls 0.3.0 — the call engine of the iOS and Android SDKs 0.8.0:

  • Calls start at once on the configuration already kept on the device; it is refreshed behind the call.
  • iOS: one 20-second deadline covers everything before the conversation opens — "The call could not connect within 20 seconds." — instead of steps that could add up to most of a minute; a call whose audio the system never hands over now fails with "The call's audio could not start." instead of staying silent.
  • Android: a connected call where nothing arrives for 50 seconds now ends (ended, reason: "failed") instead of staying open and silent; a slow network never holds the call up (at most 300 ms of the customer's voice waits to be sent, the oldest dropped); hanging up lets go of the microphone at once.

Every release: Changelog.

Every symbol, A to Z

Every public symbol of the React Native SDK — 1259 in all — by name, each linked to where this page documents it: a type or function first, then its members.

On this page

Requirements, install, permissions and initialisationPermissions and capabilitiesrequestMicrophonePermissionInitialiseWebRTC callsInstall and the first answerGraineProviderGraineProviderPropsGraineInAppClientGraineInAppOptionsGraineStorageGraineInAppClient.optsGraineInAppClient.sessionSessionConfigGraineInAppClient.configGraineInAppClient.connectGraineInAppClient.isConnectedGraineInAppClient.transportGraineInAppClient.closeuseGraineAgentGraineContextValueuseGraineClientsetDefaultClientuseGraineReadyuseGraineInitIdentity, memory and conversations that continueGraineInAppClient.identifyuseGraineIdentifyGraineInAppClient.setMemoryGraineInAppClient.memoryEnabledGraineInAppClient.getIdentityGraineInAppClient.getRawTraitsGraineInAppClient.installIdGraineInAppClient.getInstallIdGraineInAppClient.identityChannelIdentityChannelDEFAULT_IDENTITY_FIELDGraineInAppClient.memoriesuseGraineMemoriesGraineMemoryGraineInAppClient.forgetMemoryGraineInAppClient.newConversationRestoredTurnChatGraineInAppClient.openChatGraineInAppClient.takeChatOpenGraineChatScreenGraineChatScreenPropsGraineInAppClient.sendTextGraineInAppClient.queuedTextsGraineInAppClient.sayGraineInAppClient.chatFailedGraineInAppClient.retryGraineInAppClient.clearChatGraineInAppClient.inTextChatuseGraineSurfaceGraineInAppClient.surfaceGraineInAppClient.setSurfaceAgentSurfaceSurfacePolicyresolveSurfaceTurnChatTurnChatStateChatEventfoldChatchatEventFromClientEMPTY_CHATCHAT_TYPING_TIMEOUT_MSCHAT_HISTORY_CAPChatRowChatRowOptionschatRowschatTurnRowschatClockchatDatevoiceCallLabelchatIsNewchatPiecesChatPiecesChatPieceTokensDEFAULT_UPCOMING_TITLEChatSizeCHAT_SIZESchatSizeOfThe voice barGraineVoiceLauncherGraineVoiceLauncherPropsGraineVoiceApiCAPTION_HISTORYCaptionGraineVoiceBarGraineVoiceBarPropsGraineBarActionGraineBarActionPropsGraineLauncherButtonGraineLauncherButtonPropsGraineFabGraineFabPropsGraineAgentBarGraineAgentBarPropsGraineBarThemeGraineLauncherGraineLauncherPropsGraineLoaderGraineLoaderPropsuseGraineLoaderGraineInAppClient.setMutedGraineInAppClient.isMutedGraineInAppClient.isAgentSpeakinguseGraineConnectionGraineInAppClient.connectionQualityGraineInAppClient.networkStateGraineInAppClient.reportQualityGraineInAppClient.reportNetworkConnectionQualityQualitySampleclassifyQualityGraineInAppClient.hasVoiceHostGraineInAppClient.setVoiceHostGraineInAppClient.setHostCallLiveuseGraineVoiceVoiceSessionVoiceSessionOptionsAudioAdapterliveAudioStreamAdapterCAPTURE_SAMPLE_RATEAGENT_PCM_RATEGraineInAppClient.sendAudioGraineInAppClient.endUtteranceGraineInAppClient.setPlayoutClockdecodeAgentAudioAudioFormatDecodedAudiodecodeMuLawdecodePcm16base64ToBytesbase64ToPcm16EchoGuardRtcVoiceSessionControllerRtcVoiceOptionsRtcVoiceSessionRtcVoiceStateuseKeyboardLiftliftForuseDraggablePlacementclampOffsetoffsetDuringDragisTapDRAG_SLOP_PXSeveral agents in one appOne provider per agentGraineInAppClient.conversationGraineInAppClient.disposeGraineInAppClient.joinDeviceGraineInAppClient.leaveDeviceGraineInAppClient.yieldLineGraineCalls.agentsDEFAULT_AGENTGraineCallsAgentOptionsGraineCallsAgentsOptionsScreen context, actions and componentsuseGraineScreenScreenContextScreenFielduseGraineFieldGraineInAppClient.setScreenGraineInAppClient.clearScreenGraineInAppClient.updateFieldGraineInAppClient.removeFieldGraineInAppClient.getScreenGraineInAppClient.buildContextFrameuseGraineTrackGraineInAppClient.trackGraineInAppClient.getRecentEventsuseGraineTapUNNAMED_TAPGraineTouchCaptureGraineTouchCapturePropsTAP_MAX_MOVE_PXTAP_MAX_MSGraineInAppClient.reportEventAppEventGraineInAppClient.noteInteractionuseGraineVariablesGraineInAppClient.setVariablesGraineInAppClient.getVariablesGraineInAppClient.allVariablesuseGraineActionGraineInAppClient.registerActionActionHandlerAppActionResultGraineInAppClient.executeActionAppActionRequestGraineInAppClient.checkActionsActionDiagnosisGraineInAppClient.availableActionsGraineInAppClient.getAvailableActionsNAVIGATE_ACTIONuseGraineHighlightGraineInAppClient.registerHighlightableGraineInAppClient.getHighlightablesGraineInAppClient.highlightuseGraineWidgetGraineWidgetGraineWidgetPropsGraineWidgetThemeGraineInAppClient.submitWidgetGraineInAppClient.pendingWidgetGraineInAppClient.clearPendingWidgetGraineInAppClient.presentWidgetGraineInAppClient.presentPendingWidgetPENDING_WIDGET_MSAWAITING_REPLY_MSwidgetSurfaceWidgetSurfaceWIDGET_SURFACE_CHOICESWidgetStyleWidgetLooknormaliseWidgetStyleresolveWidgetLookWidgetPresetWIDGET_PRESETSDEFAULT_WIDGET_PRESETWidgetImageSizeWIDGET_IMAGE_SIZESWidgetDensityWIDGET_DENSITIESWidgetButtonWIDGET_BUTTONSWidgetPlatformWIDGET_PLATFORMSPlatformGrammarPLATFORM_GRAMMARresolvePlatformWidgetSelectWIDGET_SELECTSMAX_SELECT_DEFAULTmanySummaryWidgetSelectActionnormaliseSelectActionfillSelectArgsfillSelectTemplateselectCallSELECT_ARG_MAXSELECT_VALUE_MAXresolveWidgetImagesresolveWidgetPaletteWidgetPaletteWidgetModeLoaderKindLOADER_KINDSDEFAULT_LOADER_KINDLoaderSizeLOADER_SIZESLoaderSpeedLOADER_SPEEDSLoaderMomentLOADER_MOMENTSLoaderTextDEFAULT_LOADER_TEXTLOADER_TEXT_MAXLoaderStyleLoaderLooknormaliseLoaderStyleresolveLoaderMIN_LOADER_MSFrictionConfigFrictionScreenRulesFrictionNudgeFrictionSignalFrictionKindDEFAULT_FRICTIONresolveFrictionrulesForFrictionDetectorGraineInAppClient.configureFrictionGraineInAppClient.frictionaddMaskRulemaskDeepmaskStringLauncherVisibilityLauncherGroupLauncherInsetLauncherContinuityLauncherDelayPolicyLauncherVisibilityTrackerVisibilityDecisionactiveRouteNameCalls the user placesGraineCallsGraineCalls.configureGraineCallsOptionsGraineCallTransportGraineCalls.startCallGraineStartCallOptionsGraineCalls.endGraineCalls.setMutedGraineCalls.setSpeakerGraineCalls.attachGraineAttachableClientGraineActionResultGraineInAppClient.nativeCallLiveGraineInAppClient.setNativeCallLiveGraineCalls.getSnapshotGraineCalls.subscribeuseGraineCallGraineCallSnapshotGraineCallStateGraineCaptionGraineCallOutcomeEVENTSCalls Graine ringsGraineCalls.enableVoIPPushesGraineCalls.handlePushGraineCalls.registerPushTokenGraineCalls.installIdGraineCalls.answerGraineCalls.declineGraineCalls.onIncomingCallGraineIncomingCallGraineCalls.onOutcomePermissions and the setup checkGraineCalls.callPermissionsGraineCalls.requestCallPermissionsGraineCalls.openCallSettingsGraineCalls.callSetupProblemsGraineCallPermissionGraineCallPermissionStatusGraineCallPermissionsGraineCallSetupProblemBooking with the SDK's sheetGraineBookingSheetGraineBookingSheetPropsGraineInAppClient.openBookingGraineInAppClient.bookingAvailableGraineInAppClient.bookingBlockerGraineInAppClient.takeBookingGraineInAppClient.settleBookingbookingAvailableFrombookingBlockerBookingBlockerBookingPlatformbookingBoundsBookingBoundsclampBookingbookingDaysBookingDaybookingSlotsbookingDayLabelbookingCallNamebookingSubtitlebookingZoneLinebookingDoneLinebookingErrorLineBOOKING_TEXTBOOKING_ROUND_MINUTESBOOKING_SLOT_MINUTESBOOKING_REASON_MAXThe scheduled-calls listGraineInAppClient.schedulingRulesGraineSchedulingRulesGraineInAppClient.scheduleCallGraineInAppClient.scheduledCallsGraineInAppClient.cancelScheduledCalluseGraineSchedulingGraineScheduledCallGraineCallStatusGraineCallSourceupcomingCallsupcomingTimeLabelDesignuseGraineThemeThemeTokensThemeColorsThemeShadowResolvedDesignDesignConfigDesignPresetDESIGN_PRESETSDEFAULT_DESIGN_PRESETdesignPresetnormaliseDesignConfigdesignFromAppearanceresolveTokensresolveDesignpickTokenssessionDesigncontrastRatioreadableOnDesignBubblesDesignChromeDESIGN_CHROMESLauncherShapeMotionEasingIconStyleSdkIconNameSDK_ICONSLAUNCHER_ICONSglyphUrlLogoPlateLOGO_PLATESLOGO_PLATE_WHITEnormaliseLogoPlatelogoBoxlogoPlateLookDESIGN_FONTSFontLoaderFontFacefaceForfacesForDesignfontUrlFONT_ASSET_VERSIONSvgKitresolveNativeAppearanceNativeAppearanceNativeAppearanceFallbackchatPresentationChatPresentationchatPanelSizeCHAT_WINDOWGoing liveRelease checklistGraineCalls.isAvailableGraineInAppClient.setAppVersionEvents and listenersuseGraineEventsGraineEventGraineInAppClient.onGraineInAppClient.emitErrorsGraineErrorGraineErrorCodeWhen voice cannot startRtcVoiceErrorRtcVoiceErrorCodeCalls package errorsRate limits and quotasLower-level building blocksTICKET_VARIABLE_LIMITSvariablesForTicketcontextTokenreadAgentChunkAgentChunkSTREAM_STARTSTREAM_ENDreadHistoryFrameWhat's new in 0.42.0 and calls 0.3.0Every symbol, A to Z