@graineai/inapp-react-native 0.33.5 β†’ 0.33.6

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/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export type { WidgetSelectAction } from "./protocol.js";
11
11
  export { GraineWidget } from "./react-native/widget.js";
12
12
  export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
13
13
  export type { GraineLoaderProps } from "./react-native/loader.js";
14
- export { PENDING_WIDGET_MS } from "./react-native/index.js";
14
+ export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
15
15
  export type { GraineWidgetProps, GraineWidgetTheme } from "./react-native/widget.js";
16
16
  export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
17
17
  export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type FrictionConfig, type FrictionScreenRules, type FrictionNudge, type FrictionSignal, type FrictionKind, } from "./friction.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { normaliseSelectAction, fillSelectArgs, fillSelectTemplate, selectCall,
5
5
  export { WIDGET_SELECTS, MAX_SELECT_DEFAULT, manySummary } from "./protocol.js";
6
6
  export { GraineWidget } from "./react-native/widget.js";
7
7
  export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
8
- export { PENDING_WIDGET_MS } from "./react-native/index.js";
8
+ export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
9
9
  export { GraineInAppClient, } from "./client.js";
10
10
  export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "./friction.js";
11
11
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
package/dist/protocol.js CHANGED
@@ -204,7 +204,7 @@ export function resolveLoader(style) {
204
204
  px: LOADER_PX[s.size ?? "medium"],
205
205
  periodMs: LOADER_PERIOD[s.speed ?? "normal"],
206
206
  text: { ...DEFAULT_LOADER_TEXT, ...(s.text ?? {}) },
207
- skeleton: s.skeleton ?? true,
207
+ skeleton: s.skeleton ?? false,
208
208
  };
209
209
  }
210
210
  const ACTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
@@ -71,6 +71,7 @@ export type GraineEvent = {
71
71
  };
72
72
  export declare const PENDING_WIDGET_MS = 12000;
73
73
  export declare const AWAITING_REPLY_MS = 12000;
74
+ export declare const MIN_LOADER_MS = 450;
74
75
  export interface GraineWidget {
75
76
  id: string;
76
77
  kind: string;
@@ -10,6 +10,7 @@ export { widgetSurface } from "../protocol.js";
10
10
  import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
11
11
  export const PENDING_WIDGET_MS = 12000;
12
12
  export const AWAITING_REPLY_MS = 12000;
13
+ export const MIN_LOADER_MS = 450;
13
14
  export const Ctx = createContext(null);
14
15
  let defaultClient = null;
15
16
  export function setDefaultClient(client) {
@@ -128,6 +129,12 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
128
129
  openRef.current = open;
129
130
  useEffect(() => {
130
131
  const offs = [
132
+ client.on("call_state", (s) => {
133
+ if (client.isConnected)
134
+ return;
135
+ setConnected(!!s?.connected);
136
+ setConnecting(!!s?.connecting);
137
+ }),
131
138
  client.on("open", () => {
132
139
  setConnected(true);
133
140
  setConnecting(false);
@@ -406,6 +413,8 @@ export function useGraineWidget() {
406
413
  const [answered, setAnswered] = useState(null);
407
414
  const [loading, setLoading] = useState(null);
408
415
  const loadingTimer = useRef(null);
416
+ const shownAt = useRef(null);
417
+ const minTimer = useRef(null);
409
418
  const [awaitingReply, setAwaitingReply] = useState(false);
410
419
  const [closed, setClosed] = useState(null);
411
420
  const awaitingTimer = useRef(null);
@@ -414,6 +423,8 @@ export function useGraineWidget() {
414
423
  clearTimeout(loadingTimer.current);
415
424
  if (awaitingTimer.current)
416
425
  clearTimeout(awaitingTimer.current);
426
+ if (minTimer.current)
427
+ clearTimeout(minTimer.current);
417
428
  }, []);
418
429
  useEffect(() => {
419
430
  const done = () => {
@@ -439,6 +450,7 @@ export function useGraineWidget() {
439
450
  if (pending)
440
451
  setWidget((cur) => (cur && cur.id === pending.id ? cur : pending));
441
452
  const offPending = client.on("widget_pending", (hint) => {
453
+ shownAt.current = Date.now();
442
454
  setLoading({ id: String(hint?.widget_id ?? ""), kind: String(hint?.kind ?? "") });
443
455
  if (loadingTimer.current)
444
456
  clearTimeout(loadingTimer.current);
@@ -452,10 +464,21 @@ export function useGraineWidget() {
452
464
  clearTimeout(loadingTimer.current);
453
465
  loadingTimer.current = null;
454
466
  }
455
- setLoading(null);
456
- setClosed(null);
457
- setWidget(next);
458
- setAnswered(null);
467
+ const show = () => {
468
+ shownAt.current = null;
469
+ setLoading(null);
470
+ setClosed(null);
471
+ setWidget(next);
472
+ setAnswered(null);
473
+ };
474
+ const seen = shownAt.current === null ? MIN_LOADER_MS : Date.now() - shownAt.current;
475
+ if (seen >= MIN_LOADER_MS)
476
+ show();
477
+ else {
478
+ if (minTimer.current)
479
+ clearTimeout(minTimer.current);
480
+ minTimer.current = setTimeout(show, MIN_LOADER_MS - seen);
481
+ }
459
482
  });
460
483
  return () => { offPending(); offWidget(); };
461
484
  }, [client, adopt]);
@@ -490,6 +513,11 @@ export function useGraineWidget() {
490
513
  awaitingTimer.current = setTimeout(() => setAwaitingReply(false), AWAITING_REPLY_MS);
491
514
  }, [client, widget]);
492
515
  const dismiss = useCallback(() => {
516
+ if (minTimer.current) {
517
+ clearTimeout(minTimer.current);
518
+ minTimer.current = null;
519
+ }
520
+ shownAt.current = null;
493
521
  if (widget) {
494
522
  client.reportEvent({
495
523
  name: "component_dismissed",
@@ -1,7 +1,8 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
3
+ import { Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
+ import { GraineLoader } from "./loader.js";
5
6
  import { resolveNativeAppearance } from "./appearance.js";
6
7
  const DARK = {
7
8
  surface: "#17161A",
@@ -40,6 +41,8 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
40
41
  const [draft, setDraft] = useState("");
41
42
  const scrollRef = useRef(null);
42
43
  const last = messages[messages.length - 1];
44
+ const waiting = connecting;
45
+ const waitingMoment = "connecting";
43
46
  const status = error
44
47
  ? "unavailable"
45
48
  : connecting ? "connecting…"
@@ -74,7 +77,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
74
77
  ], children: [expanded && (_jsxs(View, { style: styles.panel, children: [_jsxs(ScrollView, { ref: scrollRef, style: styles.transcript, contentContainerStyle: styles.transcriptInner, showsVerticalScrollIndicator: false, children: [messages.length === 0 && (_jsx(Text, { style: styles.empty, children: "I can see what you're working on \u2014 ask me anything, or I'll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
75
78
  styles.bubble,
76
79
  m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
77
- ], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }), connecting ? _jsx(ActivityIndicator, { size: "small", color: accent }) : null, _jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "πŸ”‡" : "πŸŽ™" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "βŒ„" : "βŒƒ" }) })] })] }));
80
+ ], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), waiting ? null : (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "πŸ”‡" : "πŸŽ™" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "βŒ„" : "βŒƒ" }) })] }))] })] }));
78
81
  }
79
82
  export const GraineLauncher = GraineAgentBar;
80
83
  const makeStyles = (t) => StyleSheet.create({
@@ -236,6 +236,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
236
236
  endedNotifiedRef.current = false;
237
237
  setConnected(!!msg.connected);
238
238
  setConnecting(!!msg.connecting);
239
+ client.emit?.("call_state", { connected: !!msg.connected, connecting: !!msg.connecting });
239
240
  if (!msg.connected && !msg.connecting)
240
241
  startedRef.current = false;
241
242
  onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
@@ -123,16 +123,19 @@ export function GraineWidget(props) {
123
123
  const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, awaiting: awaitingReply, onClear: dismiss })) : null;
124
124
  if (!widget && loading) {
125
125
  const loader = resolveLoader(appearance?.loader);
126
- const pendingSurface = loader.skeleton ? widgetSurface(loading.kind) : "card";
127
- const bone = (w, h, extra = {}) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6), ...extra } }));
128
- const bones = (_jsxs(View, { style: { gap: 10 }, accessibilityLabel: loader.text.component, children: [[0, 1, 2].map((i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))), _jsx(View, { style: { alignItems: "center", paddingTop: 6 }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
126
+ const kindSurface = widgetSurface(loading.kind);
127
+ const pendingSurface = kindSurface;
128
+ const big = pendingSurface === "screen" ? 34 : pendingSurface === "sheet" ? 26 : undefined;
129
+ const mark = (_jsx(View, { style: { alignItems: "center", gap: 10 }, children: _jsx(GraineLoader, { moment: "component", size: big, stacked: true }) }));
130
+ const bone = (w, h) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6) } }));
131
+ const bones = (rows) => (_jsx(View, { style: { gap: 10, opacity: 0.55 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: Array.from({ length: rows }, (_, i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))) }));
129
132
  if (pendingSurface === "screen") {
130
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsx(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 64, paddingHorizontal: 18 }, children: bones }) })] }));
133
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 56, paddingHorizontal: 18 }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: { width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: tint(theme.border, 0.4) }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "\u00D7" }) }), _jsx(View, { style: { flex: 1, justifyContent: "center", paddingBottom: loader.skeleton ? 24 : 72 }, children: mark }), loader.skeleton ? bones(3) : null] }) })] }));
131
134
  }
132
135
  if (pendingSurface === "sheet") {
133
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsx(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: _jsx(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), padding: 18, paddingBottom: 28 }, children: bones }) }) })] }));
136
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 18 }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), _jsx(View, { style: { paddingVertical: loader.skeleton ? 10 : 34 }, children: mark }), loader.skeleton ? bones(2) : null] })] }) })] }));
134
137
  }
135
- return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 12, marginBottom: 8 }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
138
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 14, marginBottom: 8, alignItems: "center" }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
136
139
  }
137
140
  if (!widget)
138
141
  return chosen;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.33.5",
3
+ "version": "0.33.6",
4
4
  "type": "module",
5
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.",
6
6
  "license": "MIT",