@graineai/inapp-react-native 0.21.0 → 0.23.0

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
@@ -765,6 +765,39 @@ and resamples in its output stage, which is where the artefacts came from.
765
765
 
766
766
  ---
767
767
 
768
+ ## Coming from RevRag
769
+
770
+ Same shape, different names. If you have a RevRag integration, this is the
771
+ whole translation:
772
+
773
+ | RevRag | Graine |
774
+ | --- | --- |
775
+ | `useInitialize({ apiKey })` → `{ isInitialized, error }` | `useGraineReady()` → `{ ready, error }` (the key goes on `GraineProvider`) |
776
+ | `EmbedProvider navigationRef appVersion` | `GraineProvider navigationRef appVersion` |
777
+ | `includeScreens` | `includeScreens` |
778
+ | `embedButtonDelayMs` | `launcherDelayMs` |
779
+ | `embedButtonVisibilityConfig` `{ defaultDelayMs, defaultInset, groups }` | `visibility` `{ defaultDelayMs, defaultInset, groups }` |
780
+ | `EmbedButtonGroupConfig` `{ id, screens, continuity, delayMs, delayPolicy, inset }` | `LauncherGroup` — identical fields |
781
+ | `continuous` / `perScreen` | same |
782
+ | `perScreen` / `oncePerGroupEntry` / `oncePerAppSession` | same |
783
+ | `EmbedButtonInset` | `LauncherInset` — same shape |
784
+ | `EmbedButton` (mount it yourself) | `GraineLauncher` |
785
+ | `Embed.Event(USER_DATA, { app_user_id, data })` | `useGraineIdentify()` / `client.identify({ id, name, … })` |
786
+ | `Embed.Event(SCREEN_STATE, { screen, data })` | `useGraineScreen({ screen, fields })` |
787
+ | `Embed.Event(CUSTOM_EVENT, data)` / `ANALYTICS_DATA` | `useGraineTrack()` / `client.track(name, data)` |
788
+ | `embedOnAgent(AGENT_CONVERSATION_STARTED / ENDED)` | `useGraineEvents(e => e.type === "conversation_started" / "conversation_ended")` |
789
+ | `AgentEvent.MICROPHONE_PERMISSION_DENIED` | `{ type: "mic_denied", reason }` on the same stream |
790
+ | `AgentEvent.POPUP_MESSAGE_VISIBLE` | `{ type: "launcher_shown", screen }` |
791
+ | `checkPermissions()` | `requestMicrophonePermission()` |
792
+ | server `widget_config` | `appearance` from the session, via `resolveNativeAppearance` |
793
+ | LiveKit native setup | none — see *Why there is no LiveKit step* |
794
+
795
+ Two things RevRag has that are deliberately absent: a LiveKit install step
796
+ (audio rides a WebView on a hosted page, so audio fixes ship without an app
797
+ release), and click tracking on every touchable by default (`useGraineTap` is
798
+ opt-in per element, because a stream of every tap is noise the agent has to be
799
+ told to ignore).
800
+
768
801
  ## Reference
769
802
 
770
803
  | Hook | For |
package/dist/client.d.ts CHANGED
@@ -17,6 +17,16 @@ export interface SessionConfig {
17
17
  appearance?: Record<string, any>;
18
18
  variables?: Record<string, string>;
19
19
  components?: any[];
20
+ appActions?: Array<{
21
+ tool_name?: string;
22
+ toolName?: string;
23
+ name?: string;
24
+ }>;
25
+ }
26
+ export interface ActionDiagnosis {
27
+ usable: string[];
28
+ undeclared: string[];
29
+ unhandled: string[];
20
30
  }
21
31
  export declare class GraineInAppClient {
22
32
  readonly opts: GraineInAppOptions;
@@ -40,12 +50,15 @@ export declare class GraineInAppClient {
40
50
  private muted;
41
51
  private playoutClock;
42
52
  private identity;
53
+ private appVersion;
43
54
  private recentEvents;
44
55
  constructor(opts: GraineInAppOptions);
45
56
  on(event: string, fn: Listener): () => void;
46
- private emit;
57
+ emit(event: string, payload?: any): void;
47
58
  private get fetch();
48
59
  session(): Promise<SessionConfig>;
60
+ checkActions(): ActionDiagnosis;
61
+ private reportActionMismatch;
49
62
  private ticket;
50
63
  connect(): Promise<void>;
51
64
  private openSocket;
@@ -72,6 +85,7 @@ export declare class GraineInAppClient {
72
85
  reportEvent(event: AppEvent): void;
73
86
  track(name: string, data?: Record<string, unknown>): void;
74
87
  private scheduleScreen;
88
+ setAppVersion(version: string | null | undefined): void;
75
89
  buildContextFrame(): {
76
90
  type: "app_context";
77
91
  context: Record<string, unknown>;
package/dist/client.js CHANGED
@@ -24,6 +24,7 @@ export class GraineInAppClient {
24
24
  this.muted = false;
25
25
  this.playoutClock = null;
26
26
  this.identity = {};
27
+ this.appVersion = null;
27
28
  this.recentEvents = [];
28
29
  if (!opts?.publishableKey)
29
30
  throw new Error("[Graine] publishableKey is required.");
@@ -68,8 +69,37 @@ export class GraineInAppClient {
68
69
  throw new Error(data?.error || `Embed rejected (${res.status})`);
69
70
  }
70
71
  this.config = data;
72
+ this.reportActionMismatch();
71
73
  return this.config;
72
74
  }
75
+ checkActions() {
76
+ const registered = [...this.actions.keys()];
77
+ const declared = new Set((this.config?.appActions ?? [])
78
+ .map((a) => a?.tool_name || a?.toolName || a?.name)
79
+ .filter((n) => Boolean(n)));
80
+ return {
81
+ usable: registered.filter((n) => declared.has(n)),
82
+ undeclared: registered.filter((n) => !declared.has(n)),
83
+ unhandled: [...declared].filter((n) => !this.actions.has(n)),
84
+ };
85
+ }
86
+ reportActionMismatch() {
87
+ if (this.actions.size === 0)
88
+ return;
89
+ const { undeclared, unhandled } = this.checkActions();
90
+ if (!undeclared.length && !unhandled.length)
91
+ return;
92
+ this.emit("diagnostic", { type: "actions", ...this.checkActions() });
93
+ if (undeclared.length) {
94
+ console.warn(`[Graine] This app registered ${undeclared.length} action(s) the agent is not ` +
95
+ `configured to call, so the agent cannot use them: ${undeclared.join(", ")}. ` +
96
+ `Add them to the agent's app_actions in Embed & Widgets.`);
97
+ }
98
+ if (unhandled.length) {
99
+ console.warn(`[Graine] The agent is configured to call ${unhandled.length} action(s) this ` +
100
+ `build does not implement: ${unhandled.join(", ")}. It will be refused mid-sentence.`);
101
+ }
102
+ }
73
103
  async ticket() {
74
104
  try {
75
105
  const res = await this.fetch(`${this.opts.baseUrl}/api/embed/ticket`, {
@@ -341,6 +371,10 @@ export class GraineInAppClient {
341
371
  }
342
372
  }, CONTEXT_THROTTLE_MS);
343
373
  }
374
+ setAppVersion(version) {
375
+ const v = version == null ? "" : String(version).trim();
376
+ this.appVersion = v || null;
377
+ }
344
378
  buildContextFrame() {
345
379
  const hasIdentity = Object.keys(this.identity).length > 0;
346
380
  if (!this.screen && !hasIdentity && this.recentEvents.length === 0)
@@ -352,6 +386,7 @@ export class GraineInAppClient {
352
386
  ...rest,
353
387
  ...(this.screen ? { idle_ms: Date.now() - this.screenSince } : {}),
354
388
  ...(Object.keys(this.identity).length ? { user: this.identity } : {}),
389
+ ...(this.appVersion ? { app_version: this.appVersion } : {}),
355
390
  ...(this.recentEvents.length ? { recent_events: this.recentEvents } : {}),
356
391
  ...(this.highlightables.size ? { highlightable: [...this.highlightables] } : {}),
357
392
  }),
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol.js";
2
- export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client.js";
2
+ export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
3
3
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
4
4
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } 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, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
6
+ 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 { requestMicrophonePermission } from "./react-native/permissions.js";
7
8
  export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
8
9
  export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
9
10
  export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- export { GraineInAppClient } from "./client.js";
1
+ export { GraineInAppClient, } from "./client.js";
2
2
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
3
3
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
4
4
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
5
- export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, } from "./react-native/index.js";
5
+ export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, } from "./react-native/index.js";
6
+ export { requestMicrophonePermission } from "./react-native/permissions.js";
6
7
  export { resolveNativeAppearance, } from "./react-native/appearance.js";
7
8
  export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
8
9
  export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
@@ -21,6 +21,7 @@ interface GraineContextValue {
21
21
  launcherInset: LauncherInset;
22
22
  currentScreen: string | null;
23
23
  appearance: Record<string, any> | null;
24
+ sessionReady: boolean;
24
25
  onEvent: (fn: (e: GraineEvent) => void) => () => void;
25
26
  track: (name: string, data?: Record<string, unknown>) => void;
26
27
  identify: (user: Record<string, unknown> | null) => void;
@@ -49,6 +50,12 @@ export type GraineEvent = {
49
50
  } | {
50
51
  type: "error";
51
52
  message: string;
53
+ } | {
54
+ type: "mic_denied";
55
+ reason: string;
56
+ } | {
57
+ type: "launcher_shown";
58
+ screen: string | null;
52
59
  };
53
60
  export interface GraineWidget {
54
61
  id: string;
@@ -75,11 +82,12 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
75
82
  navigationRef?: {
76
83
  current: any;
77
84
  } | null;
85
+ appVersion?: string;
78
86
  includeScreens?: string[];
79
87
  launcherDelayMs?: number;
80
88
  visibility?: LauncherVisibility;
81
89
  }
82
- export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
90
+ export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, appVersion, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
83
91
  export declare function useGraineAgent(): GraineContextValue;
84
92
  export declare function useGraineEvents(handler: (e: GraineEvent) => void): void;
85
93
  export declare function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void;
@@ -101,4 +109,8 @@ export declare function useGraineWidget(): {
101
109
  dismiss: () => void;
102
110
  };
103
111
  export declare function useGraineAction(name: string, handler: ActionHandler): void;
112
+ export declare function useGraineReady(): {
113
+ ready: boolean;
114
+ error: string | null;
115
+ };
104
116
  export {};
@@ -5,7 +5,7 @@ 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
- export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
8
+ export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, appVersion, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
9
9
  const clientRef = useRef(null);
10
10
  if (!clientRef.current) {
11
11
  clientRef.current =
@@ -37,6 +37,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
37
37
  return () => { listenersRef.current.delete(fn); };
38
38
  }, []);
39
39
  const [appearance, setAppearance] = useState(null);
40
+ const [sessionReady, setSessionReady] = useState(false);
40
41
  const [currentScreen, setCurrentScreen] = useState(null);
41
42
  const [launcherVisible, setLauncherVisible] = useState(false);
42
43
  const [launcherInset, setLauncherInset] = useState({ right: 16, bottom: 20 });
@@ -76,12 +77,14 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
76
77
  }
77
78
  if (decision.delayMs <= 0) {
78
79
  setLauncherVisible(true);
80
+ emitEvent({ type: "launcher_shown", screen: route });
79
81
  return;
80
82
  }
81
83
  setLauncherVisible(false);
82
84
  showTimerRef.current = setTimeout(() => {
83
85
  showTimerRef.current = null;
84
86
  setLauncherVisible(true);
87
+ emitEvent({ type: "launcher_shown", screen: route });
85
88
  }, decision.delayMs);
86
89
  };
87
90
  apply();
@@ -95,6 +98,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
95
98
  nav.removeListener("state", apply);
96
99
  };
97
100
  }, [navigationRef, client]);
101
+ useEffect(() => { client.setAppVersion(appVersion); }, [client, appVersion]);
98
102
  useEffect(() => {
99
103
  if (!navigationRef?.current)
100
104
  setLauncherVisible(true);
@@ -129,6 +133,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
129
133
  return [...prev, { role: "agent", text, live: true }];
130
134
  })),
131
135
  client.on("widget", (widget) => setWidgets((prev) => [...prev, widget])),
136
+ client.on("mic_denied", (p) => emitEvent({ type: "mic_denied", reason: String(p?.reason || "NotAllowedError") })),
132
137
  client.on("muted", (m) => {
133
138
  setMutedState(m);
134
139
  emitEvent({ type: "muted", muted: m });
@@ -153,6 +158,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
153
158
  const cfg = client.config ?? (await client.session());
154
159
  if (cfg?.appearance)
155
160
  setAppearance(cfg.appearance);
161
+ setSessionReady(true);
156
162
  }
157
163
  catch {
158
164
  }
@@ -234,11 +240,12 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
234
240
  launcherInset,
235
241
  currentScreen,
236
242
  appearance,
243
+ sessionReady,
237
244
  onEvent,
238
245
  track: (name, data) => client.track(name, data),
239
246
  identify: (user) => client.identify(user),
240
247
  }), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
241
- launcherVisible, launcherInset, currentScreen, onEvent, appearance]);
248
+ launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady]);
242
249
  return _jsx(Ctx.Provider, { value: value, children: children });
243
250
  }
244
251
  function useGraine() {
@@ -368,3 +375,7 @@ export function useGraineAction(name, handler) {
368
375
  handlerRef.current = handler;
369
376
  useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
370
377
  }
378
+ export function useGraineReady() {
379
+ const { sessionReady, error } = useGraineAgent();
380
+ return { ready: sessionReady, error };
381
+ }
@@ -0,0 +1,5 @@
1
+ export declare function requestMicrophonePermission(rationale?: {
2
+ title?: string;
3
+ message?: string;
4
+ buttonPositive?: string;
5
+ }): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ import { PermissionsAndroid, Platform } from "react-native";
2
+ export async function requestMicrophonePermission(rationale = {}) {
3
+ if (Platform.OS !== "android")
4
+ return true;
5
+ try {
6
+ const perm = PermissionsAndroid?.PERMISSIONS?.RECORD_AUDIO;
7
+ if (!perm)
8
+ return true;
9
+ if (await PermissionsAndroid.check(perm))
10
+ return true;
11
+ const res = await PermissionsAndroid.request(perm, {
12
+ title: rationale.title ?? "Microphone",
13
+ message: rationale.message ?? "Allow the microphone so you can talk to the assistant.",
14
+ buttonPositive: rationale.buttonPositive ?? "Allow",
15
+ });
16
+ return res === PermissionsAndroid.RESULTS.GRANTED;
17
+ }
18
+ catch {
19
+ return true;
20
+ }
21
+ }
@@ -1,7 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
- import { AppState, PermissionsAndroid, Platform, View } from "react-native";
3
+ import { AppState, View } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
+ import { requestMicrophonePermission } from "./permissions.js";
5
6
  export function GraineVoiceLauncher({ webView: WebView, autoStart = false, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }) {
6
7
  const { client, appearance } = useGraineAgent();
7
8
  const ref = useRef(null);
@@ -34,26 +35,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
34
35
  })();
35
36
  return () => { cancelled = true; };
36
37
  }, [client, voiceAgentId]);
37
- const ensureMic = useCallback(async () => {
38
- if (Platform.OS !== "android")
39
- return true;
40
- try {
41
- const perm = PermissionsAndroid?.PERMISSIONS?.RECORD_AUDIO;
42
- if (!perm)
43
- return true;
44
- if (await PermissionsAndroid.check(perm))
45
- return true;
46
- const res = await PermissionsAndroid.request(perm, {
47
- title: "Microphone",
48
- message: "Allow the microphone so you can talk to the assistant.",
49
- buttonPositive: "Allow",
50
- });
51
- return res === PermissionsAndroid.RESULTS.GRANTED;
52
- }
53
- catch {
54
- return true;
55
- }
56
- }, []);
38
+ const ensureMic = useCallback(() => requestMicrophonePermission(), []);
57
39
  const pushContext = useCallback(() => {
58
40
  const frame = client.buildContextFrame();
59
41
  if (!frame)
@@ -80,6 +62,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
80
62
  if (!(await ensureMic())) {
81
63
  setConnecting(false);
82
64
  onMicDenied?.("NotAllowedError");
65
+ client.emit("mic_denied", { reason: "NotAllowedError" });
83
66
  return;
84
67
  }
85
68
  post({ type: "graine:start-call" });
@@ -155,6 +138,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
155
138
  onEnded?.();
156
139
  return;
157
140
  case "graine:mic-denied":
141
+ client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
158
142
  onMicDenied?.(String(msg.reason || "denied"));
159
143
  return;
160
144
  case "graine:widget": {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
- "description": "Graine in-app agent for React Native \u2014 an agent that sees the screen your customer is on and can act on it.",
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",
7
7
  "private": false,
8
8
  "main": "dist/index.js",