@graineai/inapp-react-native 0.40.1 → 0.40.2

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/dist/client.d.ts CHANGED
@@ -86,6 +86,9 @@ export declare class GraineInAppClient {
86
86
  private voiceHost;
87
87
  get hasVoiceHost(): boolean;
88
88
  setVoiceHost(on: boolean): void;
89
+ private nativeCall;
90
+ get nativeCallLive(): boolean;
91
+ setNativeCallLive(live: boolean): void;
89
92
  private currentSurface;
90
93
  get surface(): AgentSurface;
91
94
  setSurface(next: AgentSurface): void;
package/dist/client.js CHANGED
@@ -95,6 +95,7 @@ export class GraineInAppClient {
95
95
  this.recentEvents = [];
96
96
  this.lastWidget = null;
97
97
  this.voiceHost = false;
98
+ this.nativeCall = false;
98
99
  this.currentSurface = "voice";
99
100
  if (!opts?.publishableKey)
100
101
  throw new Error("[Graine] publishableKey is required.");
@@ -140,6 +141,16 @@ export class GraineInAppClient {
140
141
  this.voiceHost = on;
141
142
  this.emit("voice_host", { present: on });
142
143
  }
144
+ get nativeCallLive() {
145
+ return this.nativeCall;
146
+ }
147
+ setNativeCallLive(live) {
148
+ const next = !!live;
149
+ if (this.nativeCall === next)
150
+ return;
151
+ this.nativeCall = next;
152
+ this.emit("native_call", { live: next });
153
+ }
143
154
  get surface() {
144
155
  return this.currentSurface;
145
156
  }
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
30
30
  export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, resolveWidgetPalette, type WidgetPalette, type WidgetMode, } from "./react-native/appearance.js";
31
31
  export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
32
32
  export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
33
- export { GraineVoiceBar, type GraineVoiceBarProps } from "./react-native/voice-bar.js";
33
+ export { GraineVoiceBar, GraineBarAction, type GraineVoiceBarProps, type GraineBarActionProps } from "./react-native/voice-bar.js";
34
34
  export { GraineLauncherButton, type GraineLauncherButtonProps } from "./react-native/fab.js";
35
35
  export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION, type FontLoader, type FontFace } from "./react-native/fonts.js";
36
36
  export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
21
21
  export { resolveNativeAppearance, resolveWidgetPalette, } from "./react-native/appearance.js";
22
22
  export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
23
23
  export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
24
- export { GraineVoiceBar } from "./react-native/voice-bar.js";
24
+ export { GraineVoiceBar, GraineBarAction } from "./react-native/voice-bar.js";
25
25
  export { GraineLauncherButton } from "./react-native/fab.js";
26
26
  export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION } from "./react-native/fonts.js";
27
27
  export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { type SdkIconName } from "../theme.js";
2
3
  import type { GraineVoiceApi } from "./voice-launcher.js";
3
4
  export interface GraineVoiceBarProps {
4
5
  api: GraineVoiceApi;
@@ -12,5 +13,12 @@ export interface GraineVoiceBarProps {
12
13
  bottomOffset: number;
13
14
  }) => void;
14
15
  name?: string;
16
+ accessory?: React.ReactNode;
15
17
  }
16
- export declare function GraineVoiceBar({ api, onClose, bottomInset, draggable, placement, onPlacementChange, name: nameProp, }: GraineVoiceBarProps): React.JSX.Element;
18
+ export declare function GraineVoiceBar({ api, onClose, bottomInset, draggable, placement, onPlacementChange, name: nameProp, accessory, }: GraineVoiceBarProps): React.JSX.Element;
19
+ export interface GraineBarActionProps {
20
+ label: string;
21
+ icon?: Extract<SdkIconName, "call" | "chat">;
22
+ onPress: () => void;
23
+ }
24
+ export declare function GraineBarAction({ label, icon, onPress }: GraineBarActionProps): React.JSX.Element;
@@ -55,7 +55,7 @@ function statusOf(api, isChat, chatConnected, connectingText = DEFAULT_LOADER_TE
55
55
  return { text: "Listening", waiting: null, meter: true };
56
56
  return say("Tap to talk");
57
57
  }
58
- export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, placement, onPlacementChange, name: nameProp, }) {
58
+ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, placement, onPlacementChange, name: nameProp, accessory, }) {
59
59
  const ctx = useContext(Ctx);
60
60
  const { messages, send, connected: chatConnected, appearance, surface, canSwitch, setSurface, launcherInset } = useGraineAgent();
61
61
  const look = useDesign();
@@ -92,7 +92,7 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
92
92
  return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
93
93
  position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
94
94
  bottom: baseBottom + drag.offset + keyboardLift,
95
- }, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, logoPlate: dash.logoPlate, messages: messages, send: send, online: chatConnected, lifted: keyboardLift > 0, onCollapse: () => (canSwitch ? setSurface("voice") : endCall()) })) : caption ? (_jsx(CaptionCard, { look: look, role: caption.role, text: caption.text, live: !!caption.live })) : null, _jsx(Raised, { t: t, level: "floating", radius: t.shape.radius.xl, children: _jsxs(View, { style: {
95
+ }, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, logoPlate: dash.logoPlate, messages: messages, send: send, online: chatConnected, lifted: keyboardLift > 0, onCollapse: () => (canSwitch ? setSurface("voice") : endCall()) })) : caption ? (_jsx(CaptionCard, { look: look, role: caption.role, text: caption.text, live: !!caption.live })) : null, draws(accessory) ? _jsx(View, { pointerEvents: "box-none", children: accessory }) : null, _jsx(Raised, { t: t, level: "floating", radius: t.shape.radius.xl, children: _jsxs(View, { style: {
96
96
  height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
97
97
  backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
98
98
  borderRadius: t.shape.radius.xl, ...softShadow(t, "floating"),
@@ -107,6 +107,34 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
107
107
  api.start();
108
108
  }, children: _jsx(Icon, { name: api.muted ? "micOff" : "mic", t: t, color: api.muted ? "#FFFFFF" : c.onAccent, kit: kit }) })), _jsx(RoundButton, { label: api.connected ? "End call" : "Close", fill: api.connected ? c.danger : c.accentSoft, onPress: endCall, children: _jsx(Icon, { name: api.connected ? "end" : "close", t: t, color: api.connected ? "#FFFFFF" : c.textMuted, kit: kit }) })] }) })] }));
109
109
  }
110
+ export function GraineBarAction({ label, icon = "call", onPress }) {
111
+ const ctx = useContext(Ctx);
112
+ const look = useDesign();
113
+ const t = look.tokens;
114
+ const c = t.colors;
115
+ const kit = ctx?.svg ?? null;
116
+ const unit = t.spacing.unit;
117
+ const outline = t.elevation.hardOffset ? { borderWidth: Math.max(1, t.shape.borderWidth), borderColor: c.text } : {};
118
+ return (_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: label, hitSlop: 8, onPress: onPress, style: ({ pressed }) => [
119
+ {
120
+ alignSelf: "flex-end",
121
+ flexDirection: "row",
122
+ alignItems: "center",
123
+ gap: unit * 1.5,
124
+ paddingHorizontal: unit * 3.5,
125
+ paddingVertical: unit * 2,
126
+ marginBottom: unit * 2,
127
+ borderRadius: t.shape.radius.xl,
128
+ backgroundColor: c.accent,
129
+ opacity: pressed ? 0.85 : 1,
130
+ },
131
+ outline,
132
+ softShadow(t, "floating"),
133
+ ], children: [_jsx(Icon, { name: icon, t: t, color: c.onAccent, kit: kit, size: 16 }), _jsx(Text, { style: { color: c.onAccent, fontSize: t.font.scale.body, ...look.face(t.font.body, t.font.weights.strong) }, children: label })] }));
134
+ }
135
+ function draws(node) {
136
+ return node !== null && node !== undefined && typeof node !== "boolean";
137
+ }
110
138
  function softShadow(t, level) {
111
139
  return t.elevation.hardOffset ? {} : shadowStyle(t.elevation[level]);
112
140
  }
@@ -5,6 +5,7 @@ import { Ctx, useGraineAgent } from "./index.js";
5
5
  import { requestMicrophonePermission } from "./permissions.js";
6
6
  export const CAPTION_HISTORY = 12;
7
7
  export const SLOW_CONNECT_MS = 4000;
8
+ const CALL_IN_PROGRESS = "A call is already in progress.";
8
9
  export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active: activeProp = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
9
10
  const { client, appearance, surface, surfaces } = useGraineAgent();
10
11
  const endOnBackground = useContext(Ctx)?.endOnBackground ?? true;
@@ -214,7 +215,18 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
214
215
  pushContext();
215
216
  return client.on("screen", () => pushContext());
216
217
  }, [client, ready, pushContext]);
218
+ const refusedForNativeCall = useCallback(() => {
219
+ if (!client.nativeCallLive)
220
+ return false;
221
+ pendingStartRef.current = false;
222
+ startedRef.current = false;
223
+ setConnecting(false);
224
+ onErrorRef.current?.(CALL_IN_PROGRESS);
225
+ return true;
226
+ }, [client]);
217
227
  const beginCall = useCallback(async () => {
228
+ if (refusedForNativeCall())
229
+ return;
218
230
  endedNotifiedRef.current = false;
219
231
  if (client.isConnected) {
220
232
  console.warn("[graine] The provider has its own connection open while GraineVoiceLauncher " +
@@ -228,8 +240,24 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
228
240
  client.emit("mic_denied", { reason: "NotAllowedError" });
229
241
  return;
230
242
  }
243
+ if (refusedForNativeCall())
244
+ return;
231
245
  post({ type: "graine:start-call" });
232
- }, [client, ensureMic, onMicDenied, post]);
246
+ }, [client, ensureMic, onMicDenied, post, refusedForNativeCall]);
247
+ const connectingRef = useRef(false);
248
+ connectingRef.current = connecting;
249
+ useEffect(() => client.on("native_call", (e) => {
250
+ if (!e?.live)
251
+ return;
252
+ if (pendingStartRef.current) {
253
+ pendingStartRef.current = false;
254
+ setConnecting(false);
255
+ }
256
+ if (connectedRef.current || connectingRef.current) {
257
+ post({ type: "graine:end-call" });
258
+ armEndWatchdog();
259
+ }
260
+ }), [client, post, armEndWatchdog]);
233
261
  useEffect(() => {
234
262
  if (!connected)
235
263
  return;
@@ -425,6 +453,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
425
453
  setCaptionsOn,
426
454
  clearCaptions: useCallback(() => setCaptions([]), []),
427
455
  start: useCallback(() => {
456
+ if (refusedForNativeCall())
457
+ return;
428
458
  setConnecting(true);
429
459
  setMicDenied(false);
430
460
  setFault(null);
@@ -438,7 +468,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
438
468
  return;
439
469
  }
440
470
  void beginCall();
441
- }, [beginCall, ready]),
471
+ }, [beginCall, ready, refusedForNativeCall]),
442
472
  end: useCallback(() => { post({ type: "graine:end-call" }); armEndWatchdog(); }, [post, armEndWatchdog]),
443
473
  connectivity,
444
474
  retry: restartLoad,
@@ -474,6 +504,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
474
504
  if (autoStart) {
475
505
  if (startedRef.current)
476
506
  return;
507
+ if (refusedForNativeCall())
508
+ return;
477
509
  setConnecting(true);
478
510
  setCaptions([]);
479
511
  if (ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
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",