@graineai/inapp-react-native 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INTEGRATION.md CHANGED
@@ -824,6 +824,17 @@ fourth. The guarantees, so you do not have to think about them:
824
824
  unmounting the old screen after the new one mounted cannot erase it.
825
825
  - **Reconnect-safe.** The newest context is replayed whenever the media or the
826
826
  screen channel (re)connects.
827
+ - **Self-healing.** The newest screen is re-asserted every few seconds during
828
+ a call and when the app returns to the foreground, with its original
829
+ sequence number — so a frame lost in flight is repaired within seconds, not
830
+ at the next screen change; one that arrived is simply ignored.
831
+ - **Applied every turn.** The runtime puts the last-reported screen on the
832
+ prompt at every generation, so nothing that rewrites the prompt mid-call — a
833
+ language switch, a summary — can make the agent forget where the customer is.
834
+
835
+ Every reporting hook works **outside a provider** too — against the client the
836
+ provider registered, or one you pass to `setDefaultClient(client)` once — so an
837
+ app whose provider wraps only its bar has no reason to re-implement them.
827
838
 
828
839
  If you drive the client yourself instead of using `useGraineScreen`, call
829
840
  `client.setScreen(ctx)` on mount and `client.clearScreen(ctx.screen)` on
@@ -896,6 +907,7 @@ told to ignore).
896
907
  | `useGraineAgent()` | connection state, transcript, mute, send a turn |
897
908
  | `useGraineIdentify()` | who the customer is |
898
909
  | `useGraineScreen()` | what is on the screen |
910
+ | `useGraineField()` | one input as it changes — value, error, status; friction reads it |
899
911
  | `useGraineAction()` | one handler per declared action |
900
912
  | `useGraineHighlight()` | let the agent point at an element on this screen |
901
913
  | `useGraineWidget()` | the component the agent drew, and how to answer it |
@@ -908,7 +920,7 @@ told to ignore).
908
920
 
909
921
  | Prop | Type | Notes |
910
922
  |---|---|---|
911
- | `publishableKey` | `string` | Required unless you pass `client`. |
923
+ | `publishableKey` | `string` | Required unless you pass `client`. Selects the agent — one key per agent; there is no `agentId` prop. |
912
924
  | `baseUrl` | `string` | Defaults to Graine's host. |
913
925
  | `client` | `GraineInAppClient` | Use **your** instance instead of building one. Pass this if your app has a module-level client. |
914
926
  | `navigationRef` | `ref` | The same ref you give `NavigationContainer`. Makes the agent screen-aware with no per-screen code. |
@@ -926,6 +938,7 @@ told to ignore).
926
938
  | Method | For |
927
939
  |---|---|
928
940
  | `setScreen(ctx)` / `clearScreen(id)` | Report a screen; withdraw it only if it is still the current one. |
941
+ | `updateField(field)` / `removeField(name)` | One input changed; merged into the current screen by name. |
929
942
  | `identify(user)` | Who the customer is. Masked on the way out. |
930
943
  | `track(name, data)` / `reportEvent(event)` | Inform the agent / ask it to speak up. |
931
944
  | `registerAction(name, handler)` / `executeAction(name, args)` | Implement and run in-app actions. |
package/README.md CHANGED
@@ -217,6 +217,7 @@ requests a permission on your behalf.
217
217
  | `GraineProvider` | Connects the agent. Set `voice` for spoken conversations. |
218
218
  | `useGraineAgent()` | Connection state, transcript, and `send()`. |
219
219
  | `useGraineScreen(context)` | Report what this screen shows. |
220
+ | `useGraineField(name, field)` | One input as it changes — value, error, status. |
220
221
  | `useGraineAction(name, handler)` | Implement one action the agent may call. |
221
222
  | `useGraineVoice(adapter, opts)` | Microphone, playback and barge-in. |
222
223
  | `useGraineEvents(fn)` | Conversation lifecycle, `action_completed`, `friction_detected`, `mic_denied`. |
@@ -234,8 +235,11 @@ provider only for the screens your app knows better. `DEFAULT_FRICTION`,
234
235
  `resolveFriction` and `FrictionDetector` are exported for hosts that want the
235
236
  engine on their own timers.
236
237
 
238
+ Every reporting hook works outside a provider too — call `setDefaultClient(client)`
239
+ once if your provider wraps only your bar.
240
+
237
241
  `GraineInAppClient` is exported for apps that want the transport without the
238
- React layer: `setScreen`, `clearScreen`, `identify`, `track`, `reportEvent`,
242
+ React layer: `setScreen`, `clearScreen`, `updateField`, `removeField`, `identify`, `track`, `reportEvent`,
239
243
  `registerAction`, `executeAction`, `configureFriction`, `friction`, `session`.
240
244
 
241
245
  ## Support
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext } from "./protocol.js";
1
+ import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext, type ScreenField } from "./protocol.js";
2
2
  import { type FrictionConfig } from "./friction.js";
3
3
  export declare const SUBPROTOCOL = "graine.embed.v1";
4
4
  export interface GraineInAppOptions {
@@ -91,6 +91,8 @@ export declare class GraineInAppClient {
91
91
  highlight(name: string | null, ttlMs?: number): void;
92
92
  registerAction(name: string, handler: ActionHandler): () => void;
93
93
  get availableActions(): string[];
94
+ updateField(field: ScreenField): void;
95
+ removeField(name: string): void;
94
96
  clearScreen(screenId: string | null | undefined): void;
95
97
  setScreen(context: ScreenContext | null): void;
96
98
  private dispatchFriction;
package/dist/client.js CHANGED
@@ -363,6 +363,25 @@ export class GraineInAppClient {
363
363
  get availableActions() {
364
364
  return [...this.actions.keys()];
365
365
  }
366
+ updateField(field) {
367
+ if (!this.screen || !field || !field.name)
368
+ return;
369
+ const fields = [...(this.screen.fields ?? [])];
370
+ const i = fields.findIndex((f) => f.name === field.name);
371
+ if (i >= 0)
372
+ fields[i] = { ...fields[i], ...field };
373
+ else
374
+ fields.push(field);
375
+ this.setScreen({ ...this.screen, fields });
376
+ }
377
+ removeField(name) {
378
+ if (!this.screen || !name)
379
+ return;
380
+ const fields = this.screen.fields ?? [];
381
+ if (!fields.some((f) => f.name === name))
382
+ return;
383
+ this.setScreen({ ...this.screen, fields: fields.filter((f) => f.name !== name) });
384
+ }
366
385
  clearScreen(screenId) {
367
386
  if (!this.screen)
368
387
  return;
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type Fri
4
4
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
5
5
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
6
6
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
7
- export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
7
+ export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
8
8
  export { requestMicrophonePermission } from "./react-native/permissions.js";
9
9
  export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
10
10
  export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "
3
3
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
4
4
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
5
5
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
6
- export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, } from "./react-native/index.js";
6
+ export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, } from "./react-native/index.js";
7
7
  export { requestMicrophonePermission } from "./react-native/permissions.js";
8
8
  export { resolveNativeAppearance, } from "./react-native/appearance.js";
9
9
  export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { GraineInAppClient, type GraineInAppOptions } from "../client.js";
3
3
  import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
4
- import type { ActionHandler, ScreenContext } from "../protocol.js";
4
+ import type { ActionHandler, ScreenContext, ScreenField } from "../protocol.js";
5
5
  import type { FrictionConfig } from "../friction.js";
6
6
  import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
7
7
  interface GraineContextValue {
@@ -80,6 +80,8 @@ export interface Turn {
80
80
  text: string;
81
81
  live?: boolean;
82
82
  }
83
+ export declare function setDefaultClient(client: GraineInAppClient | null): void;
84
+ export declare function useGraineClient(): GraineInAppClient;
83
85
  export interface GraineProviderProps extends Partial<GraineInAppOptions> {
84
86
  children: React.ReactNode;
85
87
  client?: GraineInAppClient;
@@ -108,6 +110,7 @@ export declare function useGraineVoice(adapter: AudioAdapter | null, options?: V
108
110
  stop: () => Promise<void>;
109
111
  };
110
112
  export declare function useGraineScreen(context: ScreenContext | null): void;
113
+ export declare function useGraineField(name: string, field: Omit<ScreenField, "name">): void;
111
114
  export declare function useGraineHighlight(name: string): {
112
115
  highlighted: boolean;
113
116
  };
@@ -5,6 +5,18 @@ import { GraineInAppClient } from "../client.js";
5
5
  import { VoiceSession } from "../voice.js";
6
6
  import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
7
7
  const Ctx = createContext(null);
8
+ let defaultClient = null;
9
+ export function setDefaultClient(client) {
10
+ defaultClient = client;
11
+ }
12
+ export function useGraineClient() {
13
+ const ctx = useContext(Ctx);
14
+ const client = ctx?.client ?? defaultClient;
15
+ if (!client) {
16
+ throw new Error("[Graine] No client. Either render inside <GraineProvider>, or call setDefaultClient(client) once at startup.");
17
+ }
18
+ return client;
19
+ }
8
20
  export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
9
21
  const clientRef = useRef(null);
10
22
  if (!clientRef.current) {
@@ -99,6 +111,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
99
111
  };
100
112
  }, [navigationRef, client]);
101
113
  useEffect(() => { client.setAppVersion(appVersion); }, [client, appVersion]);
114
+ useEffect(() => { setDefaultClient(client); }, [client]);
102
115
  useEffect(() => { if (friction !== undefined)
103
116
  client.configureFriction(friction); }, [client, friction]);
104
117
  useEffect(() => {
@@ -273,11 +286,12 @@ export function useGraineEvents(handler) {
273
286
  useEffect(() => onEvent((e) => ref.current(e)), [onEvent]);
274
287
  }
275
288
  export function useGraineTrack() {
276
- const { track } = useGraine();
277
- return track;
289
+ const ctx = useContext(Ctx);
290
+ const client = useGraineClient();
291
+ return useCallback((name, data) => (ctx ? ctx.track(name, data) : client.track(name, data)), [ctx, client]);
278
292
  }
279
293
  export function useGraineTap(name, handler, data) {
280
- const { track } = useGraine();
294
+ const track = useGraineTrack();
281
295
  const handlerRef = useRef(handler);
282
296
  handlerRef.current = handler;
283
297
  const dataRef = useRef(data);
@@ -292,7 +306,7 @@ export function useGraineTap(name, handler, data) {
292
306
  }, [name, track]);
293
307
  }
294
308
  export function useGraineIdentify() {
295
- const { client } = useGraine();
309
+ const client = useGraineClient();
296
310
  return useCallback((user) => client.identify(user), [client]);
297
311
  }
298
312
  export function useGraineVoice(adapter, options = {}) {
@@ -328,15 +342,23 @@ export function useGraineVoice(adapter, options = {}) {
328
342
  };
329
343
  }
330
344
  export function useGraineScreen(context) {
331
- const { client } = useGraine();
345
+ const client = useGraineClient();
332
346
  const serialised = JSON.stringify(context ?? null);
333
347
  useEffect(() => {
334
348
  client.setScreen(context);
335
349
  return () => client.clearScreen(context?.screen);
336
350
  }, [client, serialised]);
337
351
  }
352
+ export function useGraineField(name, field) {
353
+ const client = useGraineClient();
354
+ const serialised = JSON.stringify(field ?? null);
355
+ useEffect(() => {
356
+ client.updateField({ name, ...(field ?? {}) });
357
+ }, [client, name, serialised]);
358
+ useEffect(() => () => client.removeField(name), [client, name]);
359
+ }
338
360
  export function useGraineHighlight(name) {
339
- const { client } = useGraine();
361
+ const client = useGraineClient();
340
362
  const [highlighted, setHighlighted] = useState(false);
341
363
  useEffect(() => client.registerHighlightable(name), [client, name]);
342
364
  useEffect(() => {
@@ -357,7 +379,7 @@ export function useGraineHighlight(name) {
357
379
  return { highlighted };
358
380
  }
359
381
  export function useGraineWidget() {
360
- const { client } = useGraine();
382
+ const client = useGraineClient();
361
383
  const [widget, setWidget] = useState(null);
362
384
  useEffect(() => client.on("widget", (w) => {
363
385
  if (!w?.widget_id)
@@ -379,7 +401,7 @@ export function useGraineWidget() {
379
401
  return { widget, submit, dismiss };
380
402
  }
381
403
  export function useGraineAction(name, handler) {
382
- const { client } = useGraine();
404
+ const client = useGraineClient();
383
405
  const handlerRef = useRef(handler);
384
406
  handlerRef.current = handler;
385
407
  useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
@@ -192,6 +192,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
192
192
  clearTimeout(pending);
193
193
  pending = null;
194
194
  }
195
+ pushContext();
195
196
  return;
196
197
  }
197
198
  if (next === "background" && wasActive && !pending) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "type": "module",
5
5
  "description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
6
6
  "license": "MIT",