@ringg/react-native 0.2.0 → 0.3.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/README.md +290 -0
- package/dist/index.d.mts +392 -1
- package/dist/index.d.ts +392 -1
- package/dist/index.js +4605 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4579 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -4
package/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# @ringg/react-native
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Alpha / pre-release.** Expect rough edges and breaking changes between
|
|
4
|
+
> versions. Pin an exact version if you need stability, and please report
|
|
5
|
+
> anything you hit. See **[Known issues](#known-issues)** below.
|
|
6
|
+
|
|
7
|
+
Embeddable chat + voice-call widget for [Ringg AI](https://ringg.ai) agents —
|
|
8
|
+
the React Native implementation. Drop `<RinggWidget />` over your app for text
|
|
9
|
+
chat, voice calls, interactive components (forms, calendars, quick replies,
|
|
10
|
+
Block Kit) and a post-call feedback screen.
|
|
11
|
+
|
|
12
|
+
It runs on the same headless brain as the web widget (`@ringg/core`, bundled
|
|
13
|
+
into this package), so conversation behaviour — reconnection, typing timing,
|
|
14
|
+
message ordering, optimistic sends — is identical across platforms. Only the
|
|
15
|
+
views are native.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @ringg/react-native
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then the peer dependencies, which must be installed in **your** app so there is
|
|
24
|
+
exactly one autolinked copy of each native module:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx expo install @livekit/react-native @livekit/react-native-webrtc livekit-client react-native-svg
|
|
28
|
+
# or, without Expo:
|
|
29
|
+
npm install @livekit/react-native @livekit/react-native-webrtc livekit-client react-native-svg
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires React ≥ 18 · React Native ≥ 0.73.
|
|
33
|
+
|
|
34
|
+
**Expo Go will not work** — LiveKit ships native code, so you need a
|
|
35
|
+
[development build](https://docs.expo.dev/develop/development-builds/introduction/).
|
|
36
|
+
|
|
37
|
+
## Platform setup (required for voice)
|
|
38
|
+
|
|
39
|
+
**Expo** — add the LiveKit plugin and the permissions to `app.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"expo": {
|
|
44
|
+
"plugins": ["@livekit/react-native-expo-plugin"],
|
|
45
|
+
"ios": {
|
|
46
|
+
"infoPlist": {
|
|
47
|
+
"NSMicrophoneUsageDescription": "Voice calls use the microphone.",
|
|
48
|
+
"UIBackgroundModes": ["audio"]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"android": {
|
|
52
|
+
"permissions": [
|
|
53
|
+
"android.permission.RECORD_AUDIO",
|
|
54
|
+
"android.permission.MODIFY_AUDIO_SETTINGS",
|
|
55
|
+
"android.permission.ACCESS_NETWORK_STATE",
|
|
56
|
+
"android.permission.BLUETOOTH_CONNECT"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Bare React Native** — the same keys, by hand: `NSMicrophoneUsageDescription`
|
|
64
|
+
and `UIBackgroundModes: [audio]` in `ios/<App>/Info.plist`, and the four
|
|
65
|
+
permissions above in `android/app/src/main/AndroidManifest.xml`.
|
|
66
|
+
|
|
67
|
+
## Integrate in 3 steps
|
|
68
|
+
|
|
69
|
+
### 1 · Register the WebRTC globals
|
|
70
|
+
|
|
71
|
+
Once, at your app entry, **before** anything imports LiveKit:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// index.js
|
|
75
|
+
import { registerGlobals } from "@livekit/react-native";
|
|
76
|
+
registerGlobals();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2 · Build a transport and a controller
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { useEffect, useMemo } from "react";
|
|
83
|
+
import {
|
|
84
|
+
RinggWidget,
|
|
85
|
+
createLiveKitTransport,
|
|
86
|
+
createNativeMicPermission,
|
|
87
|
+
createRinggWidgetController,
|
|
88
|
+
} from "@ringg/react-native";
|
|
89
|
+
import { createStaticUrlResolver } from "@ringg/core";
|
|
90
|
+
|
|
91
|
+
const URLS = {
|
|
92
|
+
dev: { backendUrl: "https://calling-dev.ringg.ai/ca/api/v0", livekitUrl: "wss://ringg-ai-dev-92tubwpz.livekit.cloud" },
|
|
93
|
+
stage: { backendUrl: "https://stage-api.ringg.ai/ca/api/v0", livekitUrl: "wss://mercury.webrtc-stage.ringg.ai" },
|
|
94
|
+
prod: { backendUrl: "https://prod-api.ringg.ai/ca/api/v0", livekitUrl: "wss://mercury.webrtc.ringg.ai" },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const RinggSupport = () => {
|
|
98
|
+
const { controller, livekit } = useMemo(() => {
|
|
99
|
+
const livekit = createLiveKitTransport();
|
|
100
|
+
const controller = createRinggWidgetController(
|
|
101
|
+
{
|
|
102
|
+
agentId: "<your-agent-id>",
|
|
103
|
+
authorization: "Bearer <your-token>",
|
|
104
|
+
title: "Support",
|
|
105
|
+
description: "How can we help?",
|
|
106
|
+
defaultTab: "text", // or "audio"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
transport: livekit.transport,
|
|
110
|
+
urlResolver: createStaticUrlResolver(URLS),
|
|
111
|
+
micPermission: createNativeMicPermission(),
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
return { controller, livekit };
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Releases the microphone and the audio session — do not skip this.
|
|
118
|
+
useEffect(() => () => {
|
|
119
|
+
controller.destroy();
|
|
120
|
+
livekit.dispose();
|
|
121
|
+
}, [controller, livekit]);
|
|
122
|
+
|
|
123
|
+
return <RinggWidget controller={controller} room={livekit.room} />;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Passing `room` is optional; it only enables the in-call audio visualizer.
|
|
128
|
+
|
|
129
|
+
### 3 · Mount it over your app
|
|
130
|
+
|
|
131
|
+
`RinggWidget` renders its own floating trigger and panel over whatever is
|
|
132
|
+
behind it, so make it the **last child** of your root view:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<View style={{ flex: 1 }}>
|
|
136
|
+
<YourApp />
|
|
137
|
+
<RinggSupport />
|
|
138
|
+
</View>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Tap the trigger → the chat/voice panel opens. That is the whole integration.
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
`RinggWidgetConfig` — only `agentId` is required:
|
|
146
|
+
|
|
147
|
+
| Field | Type | Purpose |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `agentId` | `string` | your Ringg agent (**required**) |
|
|
150
|
+
| `authorization` | `string` | bearer token for your account |
|
|
151
|
+
| `title` / `description` | `string` | panel header text |
|
|
152
|
+
| `defaultTab` | `"audio" \| "text"` | which mode the panel opens in |
|
|
153
|
+
| `hideTabSelector` | `boolean` | pin the widget to one mode |
|
|
154
|
+
| `defaultExpanded` | `boolean` | open the panel on mount (no trigger) |
|
|
155
|
+
| `bypassStartScreen` | `boolean` | trigger tap starts the call directly |
|
|
156
|
+
| `bypassFeedbackScreen` | `boolean` | skip the post-call rating screen |
|
|
157
|
+
| `clientOrigin` | `string` | **required for real calls** — see below |
|
|
158
|
+
| `variables` | `Record<string, …>` | values for `{{placeholders}}` in agent prompts |
|
|
159
|
+
| `theme` | `WidgetTheme` | colours, radii, button style (gradients supported) |
|
|
160
|
+
| `logoUrl` / `logoStyles` | `string` / `PortableStyles` | branding in the header |
|
|
161
|
+
| `buttons` | `ButtonsConfig` | per-button copy, icons and styles |
|
|
162
|
+
| `legalDisclaimer` | `{ text, links }` | copy under the start buttons |
|
|
163
|
+
| `feedbackScreen` | `FeedbackScreenConfig` | rating screen copy and styling |
|
|
164
|
+
| `voiceCall` | `{ showAnimation, showTranscript }` | voice view options |
|
|
165
|
+
| `enabledSlashCommands` | `SlashCommand[]` | commands offered in the composer |
|
|
166
|
+
| `eventLogs` | `{ enabled, showIds }` | inline pills for agent-triggered actions |
|
|
167
|
+
|
|
168
|
+
`widgetPosition` and `innerWindowProps` are web-only and ignored here — the
|
|
169
|
+
panel sizes itself to the device.
|
|
170
|
+
|
|
171
|
+
## Caller identity — required for real calls
|
|
172
|
+
|
|
173
|
+
The backend allow-lists an agent's callers by the `Origin` header. A browser
|
|
174
|
+
sends it automatically; a native app sends nothing, so the webcall request is
|
|
175
|
+
rejected **before authentication is even considered**:
|
|
176
|
+
|
|
177
|
+
| response | meaning |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `400 Origin header is required` | no `clientOrigin` was set |
|
|
180
|
+
| `403 Client '…' is not allowed` | it was set, but is not on the agent's list |
|
|
181
|
+
| `401 Invalid credentials` | the token is wrong for that environment |
|
|
182
|
+
|
|
183
|
+
Pass your app's identity, and add the same string to the agent's allowed
|
|
184
|
+
clients in the dashboard:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { Platform } from "react-native";
|
|
188
|
+
import { appOrigin, createRinggWidgetController } from "@ringg/react-native";
|
|
189
|
+
|
|
190
|
+
const BUNDLE_ID = Platform.OS === "android" ? "com.acme.app" : "com.acme.App";
|
|
191
|
+
|
|
192
|
+
createRinggWidgetController(
|
|
193
|
+
{ agentId: "…", authorization: "Bearer …", clientOrigin: appOrigin(BUNDLE_ID) },
|
|
194
|
+
ports,
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`appOrigin` returns `<platform>://<bundleId>` — e.g. `android://com.acme.app`.
|
|
199
|
+
The bundle id is a parameter because React Native cannot read it without a
|
|
200
|
+
native module, and this package will not add one for a single string. Your app
|
|
201
|
+
already declares it, so a constant (or a read of your own app config) is
|
|
202
|
+
enough. `expo-application` reports the same value if you would rather ask the
|
|
203
|
+
OS — but it is native code, so adding it requires rebuilding the app, not just
|
|
204
|
+
restarting the bundler.
|
|
205
|
+
|
|
206
|
+
## Host events
|
|
207
|
+
|
|
208
|
+
The controller emits the same events as the web widget. On RN they are
|
|
209
|
+
in-memory rather than DOM events:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const unsubscribe = controller.eventBus.on("ringg:conversation_status", ({ status, mode, callId }) => {
|
|
213
|
+
analytics.track(`call_${status}`, { mode, callId });
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Events: `ringg:widget_status`, `ringg:conversation_status`,
|
|
218
|
+
`ringg:feedback_status`, `ringg:calendar_booking`,
|
|
219
|
+
`ringg:component_acknowledgement`.
|
|
220
|
+
|
|
221
|
+
## Agent-triggered app actions
|
|
222
|
+
|
|
223
|
+
Agents can fire host actions (`execute_dom_action` on the wire). On web these
|
|
224
|
+
become `CustomEvent`s; RN has no ambient event bus, so you supply the handler
|
|
225
|
+
and receive the same payload:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { createHostActionDispatcher } from "@ringg/react-native";
|
|
229
|
+
|
|
230
|
+
const ports = {
|
|
231
|
+
// ...
|
|
232
|
+
onDomAction: createHostActionDispatcher(({ name, payload }) => {
|
|
233
|
+
if (name === "open_checkout") navigation.navigate("Checkout", payload);
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Notification sound
|
|
239
|
+
|
|
240
|
+
React Native has no audio playback of its own, and every option is a native
|
|
241
|
+
module — so the widget ships **silent** rather than forcing a dependency on
|
|
242
|
+
every integrator. Wire whichever player your app already has:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { createAudioPlayer } from "expo-audio";
|
|
246
|
+
import { createNotificationPlayer } from "@ringg/react-native";
|
|
247
|
+
import { DEFAULT_CONFIG } from "@ringg/core";
|
|
248
|
+
|
|
249
|
+
const notification = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Pass it as `ports.notification`.
|
|
253
|
+
|
|
254
|
+
## Testing hooks
|
|
255
|
+
|
|
256
|
+
Every meaningful node carries a `testID` mirroring the web widget's
|
|
257
|
+
`data-ringg` name, prefixed with `ringg-` — `ringg-trigger-button`,
|
|
258
|
+
`ringg-widget-root`, `ringg-header-title`, `ringg-message-input`,
|
|
259
|
+
`ringg-end-call-confirm`, and so on. These are a contract: they will not be
|
|
260
|
+
renamed without a major version.
|
|
261
|
+
|
|
262
|
+
Two web names have no RN counterpart, because they mark screen-reader-only
|
|
263
|
+
nodes and RN has no visually-hidden text: `ringg-header-status` and
|
|
264
|
+
`ringg-connecting-label`'s `sr-only` sibling. That copy lives in
|
|
265
|
+
`accessibilityLabel` on the surrounding node instead.
|
|
266
|
+
|
|
267
|
+
## Example app
|
|
268
|
+
|
|
269
|
+
A runnable Expo harness lives in [`example/`](./example) — it is the reference
|
|
270
|
+
integration and runs fully offline (mock transport + in-process backend) when
|
|
271
|
+
no credentials are configured.
|
|
272
|
+
|
|
273
|
+
## Known issues
|
|
274
|
+
|
|
275
|
+
- **Breaking changes between releases.** APIs may shift while pre-1.0.
|
|
276
|
+
- **No frosted-glass blur.** The web widget's header and composer blur what
|
|
277
|
+
scrolls under them; RN has no blur primitive without a native dependency, so
|
|
278
|
+
those surfaces are near-opaque instead.
|
|
279
|
+
- **No gradient-filled text.** The typing indicator's shimmer sweeps a gradient
|
|
280
|
+
through the glyphs on web. RN cannot fill text with a gradient without a
|
|
281
|
+
masking dependency, so the label takes the colour that sweep averages to and
|
|
282
|
+
the motion moves into the animated ellipsis beside it.
|
|
283
|
+
- **Markdown is a subset.** Agent replies render bold, italic, inline code,
|
|
284
|
+
links, lists, headings, code blocks, blockquotes and rules. Tables are not
|
|
285
|
+
supported and render as plain text.
|
|
286
|
+
- **Voice on emulators is unreliable.** Android emulator networking often
|
|
287
|
+
cannot establish the media connection, and iOS simulators expose no
|
|
288
|
+
microphone or playout device. Test voice on a real device.
|
|
289
|
+
- **The policy-finder pack is web-only.** That integrator-specific flow is not
|
|
290
|
+
part of this package.
|