@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 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.