@graineai/inapp-react-native 0.27.2 → 0.27.4
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 +72 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +13 -1
- package/dist/react-native/index.d.ts +2 -1
- package/dist/react-native/index.js +1 -0
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -518,6 +518,42 @@ your funnel.
|
|
|
518
518
|
|
|
519
519
|
---
|
|
520
520
|
|
|
521
|
+
## Tap capture when your screens sit above the provider
|
|
522
|
+
|
|
523
|
+
`GraineProvider` wraps its children in `GraineTouchCapture`, so an app whose
|
|
524
|
+
whole tree hangs below the provider gets best-effort tap tracking for free
|
|
525
|
+
(`captureTaps`, on by default with `fab`).
|
|
526
|
+
|
|
527
|
+
That covers fewer apps than it sounds. An app that builds its client at startup
|
|
528
|
+
and registers it with `setDefaultClient` can mount the provider anywhere — often
|
|
529
|
+
deep, around a call bar or a single screen — while the screens the customer
|
|
530
|
+
actually taps sit *above* it. Capture inside the provider then wraps the bar's
|
|
531
|
+
own chrome and sees none of the taps that matter.
|
|
532
|
+
|
|
533
|
+
For that layout, mount capture at your own root. Both components are exported
|
|
534
|
+
and work without a provider above them: the client resolves through
|
|
535
|
+
`setDefaultClient`, and the launcher-visibility gate simply does not apply when
|
|
536
|
+
there is no provider context to read it from.
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
import { GraineTouchCapture } from "@graineai/inapp-react-native";
|
|
540
|
+
|
|
541
|
+
export default function App() {
|
|
542
|
+
return (
|
|
543
|
+
<SafeAreaProvider>
|
|
544
|
+
<GraineTouchCapture>
|
|
545
|
+
<YourNavigator />
|
|
546
|
+
</GraineTouchCapture>
|
|
547
|
+
</SafeAreaProvider>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Taps land in the screen frame as `tap:screen` events with the tap's position;
|
|
553
|
+
the runtime reads them as "tapped the screen" and Call History shows them on the
|
|
554
|
+
call's timeline. Named taps from `useGraineTap` are unaffected and still carry
|
|
555
|
+
their names. (0.27.4 — earlier versions did not export `GraineTouchCapture`.)
|
|
556
|
+
|
|
521
557
|
## Controlling where the launcher appears
|
|
522
558
|
|
|
523
559
|
`includeScreens` for a simple allowlist:
|
|
@@ -682,6 +718,42 @@ for you on unmount and on backgrounding.
|
|
|
682
718
|
|
|
683
719
|
---
|
|
684
720
|
|
|
721
|
+
## How long "Connecting…" takes, and why
|
|
722
|
+
|
|
723
|
+
From the tap to the agent's first word there are four waits. Three of them
|
|
724
|
+
are paid before the tap on a current SDK; the fourth is bounded. If the
|
|
725
|
+
launcher sits on "Connecting…" for more than about two seconds, one of these
|
|
726
|
+
is not happening, and the table says which.
|
|
727
|
+
|
|
728
|
+
| Wait | What it is | When it is paid | Typical |
|
|
729
|
+
|---|---|---|---|
|
|
730
|
+
| Page load | The voice engine is a page in a WebView | **Before the tap** — `GraineFab` and the provider mount the engine on open and keep it warm (0.27.2+) | ~4s, hidden |
|
|
731
|
+
| Mint + REGISTER | A 120-second SIP credential, then registration on the SBC | **Before the tap**, re-warmed as it nears expiry | ~1.6s, hidden |
|
|
732
|
+
| Microphone | The OS permission prompt, first time only | At the tap | 0 once granted |
|
|
733
|
+
| INVITE → answer | The call itself | At the tap | ~0.3s |
|
|
734
|
+
|
|
735
|
+
**The INVITE is bounded, not left to the browser.** WebRTC gathers network
|
|
736
|
+
candidates before dialling, and Chrome does not report gathering complete
|
|
737
|
+
until STUN has been tried on *every* interface a device has — a VPN tunnel,
|
|
738
|
+
a link-local IPv6 address, a cellular path the app may not use. Those never
|
|
739
|
+
answer, and Chrome retries each for about forty seconds before giving up.
|
|
740
|
+
Measured on the embed page with the SIP frames timestamped: registered at
|
|
741
|
+
8.2s, the usable candidate in hand at 8.3s, the INVITE not sent until 48.3s,
|
|
742
|
+
then answered in 300ms. On a phone with Wi-Fi and cellular that is the
|
|
743
|
+
ten-second "Connecting…" that was reported. The engine now dials the moment a
|
|
744
|
+
server-reflexive candidate exists, or 1.5 seconds after gathering starts if
|
|
745
|
+
none arrives — a network with no STUN at all still gets a call,
|
|
746
|
+
with host candidates, rather than the pause and then the same call.
|
|
747
|
+
|
|
748
|
+
**On an app that is still on 0.26.x** the first two rows are paid *after* the
|
|
749
|
+
tap: the page loads and registers only once the customer has pressed the
|
|
750
|
+
microphone, so "Connecting…" is honest and about six seconds long. Moving to
|
|
751
|
+
0.27.4 is what hides them. The dial bound needs no app change — it lives in
|
|
752
|
+
the engine page, so every install gets it when the page is deployed.
|
|
753
|
+
|
|
754
|
+
What "Connecting…" should look like on a current app, then: a tap, the
|
|
755
|
+
microphone prompt once, and the greeting inside a second.
|
|
756
|
+
|
|
685
757
|
## Troubleshooting
|
|
686
758
|
|
|
687
759
|
**Nothing renders at all, and the console says `no_origin`.**
|
package/dist/client.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export declare class GraineInAppClient {
|
|
|
79
79
|
private performAction;
|
|
80
80
|
get isConnected(): boolean;
|
|
81
81
|
getScreen(): ScreenContext | null;
|
|
82
|
+
private availableActionsFrameField;
|
|
82
83
|
getIdentity(): Record<string, string>;
|
|
83
84
|
getRecentEvents(): Array<{
|
|
84
85
|
name: string;
|
|
@@ -106,7 +107,7 @@ export declare class GraineInAppClient {
|
|
|
106
107
|
buildContextFrame(): {
|
|
107
108
|
type: "app_context";
|
|
108
109
|
context: Record<string, unknown>;
|
|
109
|
-
available_actions
|
|
110
|
+
available_actions?: string[];
|
|
110
111
|
seq: number;
|
|
111
112
|
} | null;
|
|
112
113
|
private flushScreen;
|
package/dist/client.js
CHANGED
|
@@ -327,6 +327,18 @@ export class GraineInAppClient {
|
|
|
327
327
|
getScreen() {
|
|
328
328
|
return this.screen;
|
|
329
329
|
}
|
|
330
|
+
availableActionsFrameField(declared) {
|
|
331
|
+
const usable = (declared ?? this.availableActions).filter((a) => this.actions.has(a));
|
|
332
|
+
if (usable.length)
|
|
333
|
+
return { available_actions: usable };
|
|
334
|
+
if (Array.isArray(declared))
|
|
335
|
+
return { available_actions: [] };
|
|
336
|
+
if (this.actions.size === 0) {
|
|
337
|
+
console.warn("[Graine] No in-app action handlers are registered. Falling back to the dashboard's " +
|
|
338
|
+
"catalogue so the agent is not silently disarmed — register handlers with useGraineAction.");
|
|
339
|
+
}
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
330
342
|
getIdentity() {
|
|
331
343
|
return { ...this.identity };
|
|
332
344
|
}
|
|
@@ -478,7 +490,7 @@ export class GraineInAppClient {
|
|
|
478
490
|
...(this.recentEvents.length ? { recent_events: this.recentEvents } : {}),
|
|
479
491
|
...(this.highlightables.size ? { highlightable: [...this.highlightables] } : {}),
|
|
480
492
|
}),
|
|
481
|
-
|
|
493
|
+
...this.availableActionsFrameField(availableActions),
|
|
482
494
|
seq: ++this.screenSeq,
|
|
483
495
|
};
|
|
484
496
|
}
|
|
@@ -2,6 +2,8 @@ import React from "react";
|
|
|
2
2
|
import { GraineInAppClient, type GraineInAppOptions } from "../client.js";
|
|
3
3
|
import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
|
|
4
4
|
import type { ActionHandler, ScreenContext, ScreenField } from "../protocol.js";
|
|
5
|
+
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
6
|
+
export type { GraineFabProps, GraineTouchCaptureProps } from "./fab.js";
|
|
5
7
|
import type { FrictionConfig } from "../friction.js";
|
|
6
8
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
7
9
|
interface GraineContextValue {
|
|
@@ -133,4 +135,3 @@ export declare function useGraineReady(): {
|
|
|
133
135
|
ready: boolean;
|
|
134
136
|
error: string | null;
|
|
135
137
|
};
|
|
136
|
-
export {};
|
|
@@ -4,6 +4,7 @@ import { AppState } from "react-native";
|
|
|
4
4
|
import { GraineInAppClient } from "../client.js";
|
|
5
5
|
import { VoiceSession } from "../voice.js";
|
|
6
6
|
import { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
7
|
+
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
7
8
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
8
9
|
export const Ctx = createContext(null);
|
|
9
10
|
let defaultClient = null;
|
package/package.json
CHANGED