npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@graineai/inapp-react-native

v0.44.1

Published

Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.

Readme

@graineai/inapp-react-native

A Graine AI agent that runs inside your React Native app — it sees the screen your customer is on, and can act on it.

Android and iOS from one package.

npm install @graineai/inapp-react-native

Full documentation: https://www.graine.ai/docs/in-app


Before you write any code

Add app:// to the agent's Allowed domains — dashboard → your agent → Embed & Widgets → Allowed domains.

A mobile app sends no Origin header; there is no page, so there is nothing for a browser to report. Without that entry every call is refused:

403  This endpoint must be called from a browser page.

Nothing in your code fixes that, which is why it is the first thing on this page. The SDK detects this specific failure and says so, rather than passing the server's browser-shaped message through.


Quick start

import { GraineProvider, GraineAgentBar } from "@graineai/inapp-react-native";

export default function App() {
  return (
    <GraineProvider
      baseUrl="https://www.graine.ai"
      publishableKey="pk_live_…"
      variables={{ customer_name: user.firstName }}
    >
      <RootNavigator />
      <GraineAgentBar accent="#318CE7" name="Assistant" />
    </GraineProvider>
  );
}

Your publishable key is in the dashboard under Embed & Widgets.

Mount the bar at your root, never inside a screen. The agent's job is to move the customer to the right screen — and a bar rendered inside one unmounts the instant it does, killing the conversation on the very action that proves the feature works.

It is a bar rather than a sheet for the same reason: a sheet covers the screen the agent is helping with, so the moment it says "tap Continue", Continue is behind it.

Tell the agent what the customer can see

One hook, beside the state your screen already holds:

useGraineScreen({
  screen: "signup_email",
  title: "Your email address",
  journey: { name: "Sign-up", step: 2, of: 4 },
  fields: [
    { name: "email", label: "Email", value: email,
      status: error ? "invalid" : email ? "filled" : "empty", error },
  ],
});

The description is withdrawn when the component unmounts, so the agent can never discuss a screen the customer has left.

Let the agent act

Declare it in the dashboard first — Embed & Widgets → What this agent may do in your app. Presets cover navigate, fill a field, highlight and retry. Saving there also pushes the tools to the agent; a declaration that is saved but not synced leaves the agent with the tools it had before, and looks like nothing happened.

Declared server-side and nowhere else: a client that could name its own tools could invent one and talk the model into calling it. The SDK may only narrow the list, never extend it.

Then implement a handler with the same name, where it belongs:

useGraineAction("fill_pan_from_aadhaar", async () => {
  const value = await lookupPanFromAadhaar();
  setPan(value);
  return { status: "ok", data: { pan: value } };
});

Answer honestly — ok makes the agent tell the customer the change is done. A refusal with a reason is often more useful:

return { status: "refused", message: "PAN is locked after submission." };

Answer within about five seconds. For slower work, return immediately and report the outcome through the next useGraineScreen update.

Voice

The short way: no native audio module, no rebuild, and audio fixes reach you over the air.

import { WebView } from "react-native-webview";
import * as Font from "expo-font";
import Svg, { Path } from "react-native-svg";
import { GraineProvider, GraineVoiceLauncher, GraineVoiceBar } from "@graineai/inapp-react-native";

<GraineProvider
  baseUrl="…"
  publishableKey="pk_live_…"
  navigationRef={navigationRef}
  autoConnect={false}          // the launcher owns the connection
  loadFont={Font.loadAsync}    // the SDK loads the design's typeface itself
  svg={{ Svg, Path }}          // …and draws its icons in the design's style
>
  <RootNavigator />
  <GraineVoiceLauncher
    webView={WebView}
    autoStart
    onError={(m) => console.warn("[graine]", m)}
  >
    {(api) => <GraineVoiceBar api={api} />}
  </GraineVoiceLauncher>
</GraineProvider>

react-native-webview is the only peer dependency and it autolinks. The microphone and speaker live in a hidden one-pixel WebView — your customer never sees a web page. Set autoConnect={false}: leave the provider connecting too and the agent answers twice, so the customer hears two greetings and you are billed for two conversations.

Audio travels on a WebSocket by default. Set Embed & Widgets → Call audio to High quality for WebRTC — Opus, a jitter buffer and the device's own echo canceller — which carries screen context just the same and falls back on its own if it cannot connect. See INTEGRATION.md.

Bringing your own audio

Only if you already have a native audio module and want it used instead.

<GraineProvider baseUrl="…" publishableKey="pk_live_…" voice>
const { listening, start, stop } = useGraineVoice(adapter, {
  onTranscript: (text, final) => final && setHeard(text),
});

Audio is a pluggable four-method AudioAdapter, so this package never forces a particular native audio module into your build. liveAudioStreamAdapter wraps react-native-live-audio-stream.

Capture must be 16 kHz mono PCM16. Interruption is handled for you.

Buffer before you play. A player that plays each frame as it arrives will break up: a WebSocket gives no timing guarantee, so the gap between frames varies with the network and a player with no cushion runs dry in that gap. Hold at least 150ms (MIN_PLAYOUT_BUFFER_SECONDS) before starting, and when the queue does run dry, refill to that again before resuming — otherwise one hiccup becomes continuous stuttering for the rest of the turn.

Report what you are holding through bufferedSeconds(), so the agent knows when its reply has actually been heard rather than just received.

A bigger chat window

How big the chat opens is set per agent in the dashboard — Embed & Widgets → Appearance → Chat window → Window size: Compact, Large or Full screen — and GraineVoiceBar draws it with no code:

| Size | On a phone | On a tablet | |---|---|---| | Compact | The window above the bar, as always | The same | | Large | A sheet over most of the screen, the app dimmed behind it; drag it down or tap outside to close | A 420-wide panel in the bottom corner | | Full screen | The whole screen, closed with the chevron at the top left | The whole screen |

Your app can choose otherwise, for every chat or for one:

<GraineProvider baseUrl="…" publishableKey="pk_live_…" chatSize="large">

const client = useGraineClient();
client.openChat({ size: "full" });   // opens the chat now, at this size, this once

The size passed to openChat wins for that open, then the provider's chatSize, then the dashboard's. Moving between the call and the chat keeps it. Pass safeArea={useSafeAreaInsets()} (react-native-safe-area-context) to the bar for exact insets on a sheet or a full screen.

For a chat that is a screen of your own app rather than part of the bar:

<Stack.Screen name="Help">
  {({ navigation }) => <GraineChatScreen onClose={navigation.goBack} />}
</Stack.Screen>

While it is on screen the customer is in the chat and the bar steps aside.

Privacy

Identifiers are masked on the device before any screen data is sent — PAN, Aadhaar, card and account numbers, phone numbers and email addresses. The agent is told whether a field is filled, empty or rejected, not what is in it.

Add your own formats once, at startup:

import { addMaskRule } from "@graineai/inapp-react-native/mask";

addMaskRule(/MEM-\d{6}/, "[MEMBER_ID]");

maskString(text) and maskDeep(value) from the same module run that masking — your rules included — on anything you send somewhere yourself; a Date comes back as its ISO string, as JSON would send it.

The SDK never reads the screen by itself — it reports only the fields you pass to useGraineScreen. There is no screen recording and no accessibility scraping. It never opens the microphone until you call start(), and never requests a permission on your behalf.

A publishable key inside a mobile app is public. Anyone can extract it from an APK or IPA, and the web domain allowlist does not apply to mobile apps. Rate limits, single-use connection credentials and your account's credit limits are what bound it. See the security notes before launch.

Conversations continue

A customer you identify() who comes back — later today, tomorrow, on their other phone — finds the chat where they left it, and the agent picks up from there instead of greeting them again. The chat opens with the recent messages (the last 50, kept for a while after the last one), earlier calls show as one "Voice call · 6:05 PM" row, and day lines say Today, Yesterday or the date. Nothing to set up: the provider and the SDK's chat windows draw it.

A support-style chat can start over: newConversation() (on the client, or from useGraineAgent()) hangs up, lets go of the kept conversation for this agent and customer, clears the chat and opens a fresh one with the welcome — what the agent remembers about the customer stays. The chat window's header offers it (a compose icon) while it shows an earlier conversation. Each agent in your app keeps its own conversation.

Drawing your own chat? Each turn says restored: true when it came back from an earlier conversation and channel: "voice" when it was said on a call; chatTurnRows(messages) gives the rows to draw. forgetMemory() clears the chat on screen, as signing out or another person signing in does. Anonymous customers, and customers with memory off, start a fresh chat every time.

Booking a call from your app

When your workspace has Graine ring its customers and the agent lets them book (Calls & scheduling, in the dashboard), the SDK draws the booking screen itself — "Call me at…" with no screen of your own:

const { bookingAvailable, openBooking } = useGraineAgent();   // or useGraineScheduling()

{bookingAvailable ? (
  <Button
    title="Call me"
    onPress={async () => {
      const call = await openBooking({ reason: "Card replacement" });   // null: closed without booking
      if (call) console.log("Booked", call.id, call.at);
    }}
  />
) : null}

The provider draws the sheet in the agent's design, light or dark: the day (chips from today to the furthest day the agent allows), the time (a grid of times every 15 minutes, only the ones the agent's rules allow), the phone's time zone, "What's it about? (optional)" — 120 characters, kept with the call for the agent, never in the push; reason fills it — and Book call. A refusal stays in the sheet as one line: "Pick a time at least 5 minutes from now", "Pick a time within 14 days", "That time is outside the hours we can call — pick another", "You already have 3 calls booked — cancel one first", "Sign in to book a call", "Couldn't book just now — try again". Once it is booked, the sheet says when the agent will call, and Done closes it. A sheet on a phone, a centred panel on a tablet.

Presenting it yourself: <GraineBookingSheet visible={open} onClose={…} onBooked={(call) => …} reason="…" />, inside the provider.

bookingAvailable (also client.bookingAvailable) is each agent's own: true only when Graine rings this phone (the workspace's In-app calling, for this platform), the agent lets customers book, and the customer is identified with the trait the agent's Identity setting names. The SDK's chat windows (GraineVoiceBar's and GraineChatScreen) show the customer's booked calls in an Upcoming calls card, each with Cancel, and Book a call under them — or on its own in the card's place when none are booked — where booking is available; none when the design hides the booked-calls card. To list or cancel calls in your own screens, useGraineScheduling().

Calls like WhatsApp

A call that rings the customer's phone — CallKit and VoIP pushes on iOS, a ringing call notification and call screen on Android — and goes on with the screen off comes in its own package, @graineai/inapp-react-native-calls, which runs Graine's native call engine and shares this SDK's screen context and actions (GraineCalls.attach(client)). It needs a new store build; this SDK stays over-the-air. https://www.graine.ai/docs/in-app/calling#react-native

Requirements

  • React Native 0.68+
  • React 17+

API

| | | |---|---| | GraineProvider | Connects the agent. Set voice for spoken conversations. | | useGraineAgent() | Connection state (connected, connecting, reconnecting, failed once it gave up), transcript, send(), retry() and clearChat() (the screen only). | | useGraineScreen(context) | Report what this screen shows. | | useGraineField(name, field) | One input as it changes — value, error, status. | | useGraineAction(name, handler) | Implement one action the agent may call. | | useGraineVoice(adapter, opts) | Microphone, playback and barge-in. | | useGraineEvents(fn) | Conversation lifecycle, action_completed, friction_detected, mic_denied, and error (a start that failed, or what the voice launcher reports to onError). | | useGraineTrack() / useGraineTap() | Product events and taps the agent knows about but does not interrupt for. | | useGraineIdentify() | Who the customer is — it reaches the greeting and the call record. | | useGraineHighlight() / useGraineWidget() | Let the agent point at things and draw components. | | GraineVoiceLauncher | The voice engine: WebView, microphone, WebRTC or WebSocket. | | GraineVoiceBar | The voice bar in the brand's design, as the dashboard previews it. Mount inside GraineVoiceLauncher. | | GraineChatScreen | The chat as a screen of your own app, in the brand's design. | | bookingAvailable / openBooking({ reason }) / GraineBookingSheet | Booking a call: whether to offer it, and the SDK's own booking sheet. See Booking a call from your app. | | chatSize on the provider / client.openChat({ size }) | How big the chat opens: "compact", "large" or "full". See A bigger chat window. | | fab={{ webView }} on the provider / GraineFab | The floating button, mounted for you; opens the conversation, folds back on end. | | captureTaps / GraineTouchCapture | Every tap tracked automatically — no per-button wrapping. | | useGraineInit() | { isInitialized, error }. | | GraineAgentBar | Optional default UI. Mount at the root. Replace it with your own. | | addMaskRule(pattern, token) | Mask an app-specific identifier format. |

Friction detection — the agent notices a stuck customer (idle, repeated field errors, back-and-forth, rage taps, error banners) and speaks first. Rules are JSON, per screen, configured in the dashboard; pass friction on the provider only for the screens your app knows better. DEFAULT_FRICTION, resolveFriction and FrictionDetector are exported for hosts that want the engine on their own timers.

Every reporting hook works outside a provider too — call setDefaultClient(client) once if your provider wraps only your bar.

GraineInAppClient is exported for apps that want the transport without the React layer: setScreen, clearScreen, updateField, removeField, identify, track, reportEvent, registerAction, executeAction, configureFriction, friction, session.

Support

https://www.graine.ai/docs/in-app

License

MIT