Graine AI

Changelog

Every released version of the React Native SDK, what changed, and whether it asks anything of you.

The published package is @graineai/inapp-react-native.

Nothing here needs a native rebuild. The audio path lives in a WebView pointed at a hosted page, so an SDK upgrade is npm install and an over-the-air update — which is the whole reason it is built that way.

Upgrading from anything older than 0.27.4

Go straight to the latest. Two releases in that range were never published — their fixes are folded into the ones after them — and 0.27.0 is broken.

0.31.0 — the engine warms itself, and a call cannot outlive its UI

Three fixes that only show up on a real device.

The agent starts in about a second, not several. Mounting the launcher early was always meant to do the slow work before the customer taps: mint a credential, load the SIP stack, open the socket, register. The page has done all of that behind a warm signal from the start — and the only thing that ever sent it was a host without autoStart opening its bar, by which point there is nothing left to save. With autoStart, the common integration, it was never sent at all and every open paid mint, register and invite in series in front of the customer. An idle page now warms the moment it reports ready, and re-warms itself before the credential expires. Nothing to change in your code.

A call can no longer outlive the thing showing it. Every teardown — the bar closing, the app backgrounding, the launcher unmounting — reaches the page by injecting JavaScript into the WebView, which is exactly the thing that may already be gone: destroyed on unmount, reclaimed under memory pressure, or reloaded. The message went with it and the call survived, still listening and still billing, with nothing on screen. The page now hangs up on pagehide, which needs nobody's cooperation. A backgrounded app is still the host's call to make, deliberately: on iOS, hidden also means the app switcher and a permission dialog.

A component drawn while the panel was closed is not lost

useGraineWidget now starts from whatever the agent has already drawn, and the client holds it until it is answered.

The event stream has no replay, and your widget UI almost certainly lives somewhere that unmounts — a panel, a sheet, a bar that closes. So a component the agent drew while that was closed reached nobody and was gone: the customer opened the panel to nothing, and the agent waited for an answer to a card that had never been on screen. Nothing in the logs said so, because every layer had done its job.

Nothing to change in your code. The hook seeds itself on mount.

const { widget, submit, dismiss } = useGraineWidget();   // unchanged

A card is forgotten the moment it is submitted or dismissed, and when a call starts or ends — so nothing replays something the customer already dealt with, and last call's card never opens on top of this one. client.pendingWidget and client.clearPendingWidget() are there if your host needs to drive it directly, for example to open the panel when one arrives.

0.30.0 — pressing Next with blanks is a moment, and pictures beat a list read aloud

The agent speaks up when someone moves on with something missing. A new friction rule, incompleteSubmit, watches for a tap whose label means "carry on" while a field on the screen is still blank or rejected. The agent names what is missing before the screen throws an error at them.

friction: {
  incompleteSubmit: { taps: ["next", "submit", "pay"] },   // or false to switch it off
  screens: { review: { incompleteSubmit: false } },        // pressing on here is fine
}

The default list is submit, next, continue, save, proceed, apply, confirm, done, pay, verify, matched as lowercase substrings — so submit also catches submit_application. It is set in the dashboard for every app running the agent, and an app may override it for its own screens. Nothing about it is written in the prompt or in your app: the fields you already report are what it counts. Friction rules →

Product lists render in the app. A new component kind, product_list, carries a picture, a title, up to three facts and a price per row, and a button on each one. The agent picks the moment; the customer taps a row and the choice goes back as both a sentence and structured data.

{ "kind": "product_list",
  "data": { "title": "Three that fit your budget", "selectLabel": "Compare",
            "items": [{ "id": "creta-sx", "title": "Creta SX(O)", "price": "₹19.2L",
                        "imageUrl": "https://…/creta.jpg",
                        "facts": ["6 airbags", "Panoramic sunroof", "1.5 turbo"] }] } }

Build it in Embed & Widgets, once, and it reaches a website embed and a native app the same way — the SDK relays whatever kind the dashboard sends. A new one starts filled in, with sample rows and sample pictures.

A root-relative imageUrl now resolves on a device. /samples/car-blue.png is an ordinary image on a web page and a URI with no host on a phone, where it draws an empty box and says nothing. It is resolved against the agent's own origin on the way in, so what you author is what the app shows. Showing things on screen →

Components are chosen per agent. The library stays shared, but every enabled component used to be registered as a callable tool on every agent in the workspace — so a car catalogue was a tool on a loan agent. Pick what each one may show under Embed & Widgets → On screen. Picking nothing still means everything, so no existing agent changes behaviour.

optional on a field. An empty field was always counted as work left, so a screen with an optional middle name told the agent two things were missing and the agent offered to fill one. Mark it and it stays in the frame — the agent can still fill it if asked — but out of the count, out of the new friction rule, and marked "(optional)" on its own line. A field carrying an actual error is outstanding either way.

fields: [{ name: "middle_name", label: "Middle name", status: "empty", optional: true }]

0.29.0 — the agent knows what comes next

journey.next on useGraineScreen. step and of say how far along someone is; asked "what happens after this?", an agent with only a step number invents an answer — and on a setup flow an invented step is one the customer then goes looking for.

journey: { name: "KYC", step: 3, of: 5, next: "Take a selfie" }

The runtime also now states what is outstanding2 of 4 still need attention: PAN number, Date of birth — counted from the fields you already report, so the agent answers "am I done?" without tallying a list. What the agent is actually told →

0.28.1 — an empty line is a full stop, not a line

The engine closes a turn by sending it back with empty text. 0.28.0 folded that as text, so a caption went blank a few seconds after it appeared. It settles the open turn now.

0.28.0 — the transcript belongs to the SDK

The CC button works between turns. The launcher used to hand out one caption at a time through onCaption and keep nothing, so every host that wanted a CC button accumulated the transcript itself — and all of them had the same hole: turning captions on mid-call showed an empty box, because the only line that existed was the one being said at that moment.

{api.captionsOn && api.captions.map((c, i) => (
  <Text key={i} style={{ opacity: c.live ? 0.7 : 1 }}>{c.text}</Text>
))}
<Button title="CC" onPress={() => api.setCaptionsOn(!api.captionsOn)} />
  • api.captions — every turn, oldest first, capped at CAPTION_HISTORY (12)
  • api.caption — the turn being said now, or the last one
  • api.captionsOn / setCaptionsOn — the switch, defaulting on
  • clearCaptions() — forget the transcript without ending the call

A turn arrives many times as its words come in, then once settled. Those fold into one entry, so the list holds turns rather than keystrokes. It is cleared when a call starts and when you set active={false} — a call's words outliving its call is a privacy question, not a feature.

Also: activating with autoStart now reports connecting. It did not, so a host status line read "tap to talk" for the whole second a call was being placed, inviting a second tap on a call already on its way.

onCaption is unchanged and still fires for every line. Captions and hanging up →

0.27.6 — not published

Folded into 0.28.0. Deactivating (active={false}) now posts the stop frame unconditionally, and clears connected / connecting.

It had been guarded on the launcher's own belief about whether a call was running — and those flags are cleared by the page's own frame, so any ordering where that landed first closed the UI and left the call up: still listening, still billing, with nothing on screen to stop it. And because nothing else cleared the call state, a bar reopened after such a close came back reading "listening" for a call that had ended.

0.27.5 — mount the engine before the customer needs it

GraineVoiceLauncher gains active (default true, so nothing existing changes). Mount it early with active={false} and the page loads without registering or starting anything; flip it to true when your UI opens.

Five seconds of "Connecting…" measured on the reference app: opening the bar created the WebView, loaded the page, minted a credential, registered on the SBC and only then placed the call — all in series, all in front of the customer. How long connecting takes →

0.27.4 — tap capture reaches an app's own root

GraineTouchCapture and GraineFab are exported from the package entry. GraineProvider mounts capture around its children, so an app that mounts the provider deep — around a call bar — saw taps on the bar and none on its screens. Mount it at your root instead; it needs no provider above it. Screens outside the provider →

Includes the 0.27.3 fix below.

0.27.3 — not published

Folded into 0.27.4. An empty availableActions disarmed the agent. The SDK filtered the declared catalogue through its handler registry and sent [] whenever that registry was momentarily empty — and [] on the wire means nothing is callable here, so the agent silently lost every tool for that screen. It now sends the key only when a screen states one.

0.27.2 — connecting took ten seconds

The engine mounted on the tap, so the page load and the SIP registration were both paid in front of the customer. GraineFab mounts it on open and keeps it warm; the tap costs a microphone prompt and an INVITE.

0.27.1 — the FAB overlay is a prop

0.27.0 imported react-native-screens for the FAB's iOS overlay — a native module most apps do not declare, which breaks expo export and cannot ship over the air. The overlay is a prop now, so an app that wants it passes it and an app that does not is unaffected.

Do not install 0.27.0

It cannot be delivered over the air. Use 0.27.1 or later.

0.27.0 — the provider mounts the FAB, and taps are tracked

fab on GraineProvider, captureTaps, and useGraineInit(). Broken — see 0.27.1.

0.26.2 — a screen change no longer wipes the screen

useGraineScreen put a screen-less frame on the wire on every dependency change, not just on unmount — so editing a field, or any re-render with a new object identity, told the agent there was no screen at all. Reporting and withdrawal are separate effects now: report on change, withdraw only on unmount. Fast navigation →

0.26.1 — the launcher re-pushes context on foreground

A phone that slept came back with the agent holding a screen the customer had left.

Earlier

0.23.00.26.0 — the first published SDK, the screen-context contract, friction detection, and in-app actions. Start here →