@ringg/react-native 0.0.1-alpha.9 → 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.
Files changed (118) hide show
  1. package/README.md +237 -197
  2. package/dist/index.d.mts +1355 -0
  3. package/dist/index.d.ts +1355 -0
  4. package/dist/index.js +5765 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +5705 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +35 -65
  9. package/LICENSE +0 -21
  10. package/app.plugin.js +0 -51
  11. package/lib/module/adapters/index.js +0 -4
  12. package/lib/module/adapters/index.js.map +0 -1
  13. package/lib/module/adapters/livekit-rn-adapter.js +0 -216
  14. package/lib/module/adapters/livekit-rn-adapter.js.map +0 -1
  15. package/lib/module/components/RinggWidget.js +0 -554
  16. package/lib/module/components/RinggWidget.js.map +0 -1
  17. package/lib/module/components/steps/ButtonsStep.js +0 -146
  18. package/lib/module/components/steps/ButtonsStep.js.map +0 -1
  19. package/lib/module/components/steps/CalendarStep.js +0 -190
  20. package/lib/module/components/steps/CalendarStep.js.map +0 -1
  21. package/lib/module/components/steps/ConfirmationStep.js +0 -175
  22. package/lib/module/components/steps/ConfirmationStep.js.map +0 -1
  23. package/lib/module/components/steps/FormStep.js +0 -346
  24. package/lib/module/components/steps/FormStep.js.map +0 -1
  25. package/lib/module/components/steps/InteractiveFlow.js +0 -249
  26. package/lib/module/components/steps/InteractiveFlow.js.map +0 -1
  27. package/lib/module/components/ui/ActionButton.js +0 -92
  28. package/lib/module/components/ui/ActionButton.js.map +0 -1
  29. package/lib/module/components/ui/CallControls.js +0 -168
  30. package/lib/module/components/ui/CallControls.js.map +0 -1
  31. package/lib/module/components/ui/FeedbackScreen.js +0 -180
  32. package/lib/module/components/ui/FeedbackScreen.js.map +0 -1
  33. package/lib/module/components/ui/Icons.js +0 -294
  34. package/lib/module/components/ui/Icons.js.map +0 -1
  35. package/lib/module/components/ui/MessageBubble.js +0 -80
  36. package/lib/module/components/ui/MessageBubble.js.map +0 -1
  37. package/lib/module/components/ui/TabSelector.js +0 -80
  38. package/lib/module/components/ui/TabSelector.js.map +0 -1
  39. package/lib/module/components/ui/WidgetHeader.js +0 -168
  40. package/lib/module/components/ui/WidgetHeader.js.map +0 -1
  41. package/lib/module/context/RinggProvider.js +0 -134
  42. package/lib/module/context/RinggProvider.js.map +0 -1
  43. package/lib/module/context/index.js +0 -4
  44. package/lib/module/context/index.js.map +0 -1
  45. package/lib/module/globals.d.js +0 -2
  46. package/lib/module/globals.d.js.map +0 -1
  47. package/lib/module/hooks/index.js +0 -10
  48. package/lib/module/hooks/index.js.map +0 -1
  49. package/lib/module/hooks/useRinggCall.js +0 -259
  50. package/lib/module/hooks/useRinggCall.js.map +0 -1
  51. package/lib/module/hooks/useRinggChat.js +0 -141
  52. package/lib/module/hooks/useRinggChat.js.map +0 -1
  53. package/lib/module/hooks/useRinggComponents.js +0 -236
  54. package/lib/module/hooks/useRinggComponents.js.map +0 -1
  55. package/lib/module/hooks/useRinggFeedback.js +0 -66
  56. package/lib/module/hooks/useRinggFeedback.js.map +0 -1
  57. package/lib/module/hooks/useRinggTheme.js +0 -39
  58. package/lib/module/hooks/useRinggTheme.js.map +0 -1
  59. package/lib/module/hooks/useRinggTranscripts.js +0 -70
  60. package/lib/module/hooks/useRinggTranscripts.js.map +0 -1
  61. package/lib/module/hooks/useRinggWidgetState.js +0 -51
  62. package/lib/module/hooks/useRinggWidgetState.js.map +0 -1
  63. package/lib/module/index.js +0 -56
  64. package/lib/module/index.js.map +0 -1
  65. package/lib/module/package.json +0 -1
  66. package/lib/typescript/adapters/index.d.ts +0 -2
  67. package/lib/typescript/adapters/index.d.ts.map +0 -1
  68. package/lib/typescript/adapters/livekit-rn-adapter.d.ts +0 -38
  69. package/lib/typescript/adapters/livekit-rn-adapter.d.ts.map +0 -1
  70. package/lib/typescript/components/RinggWidget.d.ts +0 -44
  71. package/lib/typescript/components/RinggWidget.d.ts.map +0 -1
  72. package/lib/typescript/components/steps/ButtonsStep.d.ts +0 -17
  73. package/lib/typescript/components/steps/ButtonsStep.d.ts.map +0 -1
  74. package/lib/typescript/components/steps/CalendarStep.d.ts +0 -16
  75. package/lib/typescript/components/steps/CalendarStep.d.ts.map +0 -1
  76. package/lib/typescript/components/steps/ConfirmationStep.d.ts +0 -16
  77. package/lib/typescript/components/steps/ConfirmationStep.d.ts.map +0 -1
  78. package/lib/typescript/components/steps/FormStep.d.ts +0 -17
  79. package/lib/typescript/components/steps/FormStep.d.ts.map +0 -1
  80. package/lib/typescript/components/steps/InteractiveFlow.d.ts +0 -23
  81. package/lib/typescript/components/steps/InteractiveFlow.d.ts.map +0 -1
  82. package/lib/typescript/components/ui/ActionButton.d.ts +0 -22
  83. package/lib/typescript/components/ui/ActionButton.d.ts.map +0 -1
  84. package/lib/typescript/components/ui/CallControls.d.ts +0 -22
  85. package/lib/typescript/components/ui/CallControls.d.ts.map +0 -1
  86. package/lib/typescript/components/ui/FeedbackScreen.d.ts +0 -24
  87. package/lib/typescript/components/ui/FeedbackScreen.d.ts.map +0 -1
  88. package/lib/typescript/components/ui/Icons.d.ts +0 -28
  89. package/lib/typescript/components/ui/Icons.d.ts.map +0 -1
  90. package/lib/typescript/components/ui/MessageBubble.d.ts +0 -17
  91. package/lib/typescript/components/ui/MessageBubble.d.ts.map +0 -1
  92. package/lib/typescript/components/ui/TabSelector.d.ts +0 -21
  93. package/lib/typescript/components/ui/TabSelector.d.ts.map +0 -1
  94. package/lib/typescript/components/ui/WidgetHeader.d.ts +0 -20
  95. package/lib/typescript/components/ui/WidgetHeader.d.ts.map +0 -1
  96. package/lib/typescript/context/RinggProvider.d.ts +0 -53
  97. package/lib/typescript/context/RinggProvider.d.ts.map +0 -1
  98. package/lib/typescript/context/index.d.ts +0 -3
  99. package/lib/typescript/context/index.d.ts.map +0 -1
  100. package/lib/typescript/hooks/index.d.ts +0 -15
  101. package/lib/typescript/hooks/index.d.ts.map +0 -1
  102. package/lib/typescript/hooks/useRinggCall.d.ts +0 -41
  103. package/lib/typescript/hooks/useRinggCall.d.ts.map +0 -1
  104. package/lib/typescript/hooks/useRinggChat.d.ts +0 -29
  105. package/lib/typescript/hooks/useRinggChat.d.ts.map +0 -1
  106. package/lib/typescript/hooks/useRinggComponents.d.ts +0 -52
  107. package/lib/typescript/hooks/useRinggComponents.d.ts.map +0 -1
  108. package/lib/typescript/hooks/useRinggFeedback.d.ts +0 -23
  109. package/lib/typescript/hooks/useRinggFeedback.d.ts.map +0 -1
  110. package/lib/typescript/hooks/useRinggTheme.d.ts +0 -40
  111. package/lib/typescript/hooks/useRinggTheme.d.ts.map +0 -1
  112. package/lib/typescript/hooks/useRinggTranscripts.d.ts +0 -30
  113. package/lib/typescript/hooks/useRinggTranscripts.d.ts.map +0 -1
  114. package/lib/typescript/hooks/useRinggWidgetState.d.ts +0 -38
  115. package/lib/typescript/hooks/useRinggWidgetState.d.ts.map +0 -1
  116. package/lib/typescript/index.d.ts +0 -44
  117. package/lib/typescript/index.d.ts.map +0 -1
  118. package/lib/typescript/package.json +0 -1
package/README.md CHANGED
@@ -1,250 +1,290 @@
1
1
  # @ringg/react-native
2
2
 
3
- Add AI voice and text agents to your React Native app with [Ringg AI](https://ringg.ai).
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.
4
6
 
5
- ## Setup
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.
6
11
 
7
- ### 1. Install
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
8
18
 
9
19
  ```bash
10
20
  npm install @ringg/react-native
11
21
  ```
12
22
 
13
- ### 2. Add the Expo plugin
14
-
15
- In your `app.json`:
16
-
17
- ```json
18
- {
19
- "expo": {
20
- "plugins": ["@ringg/react-native"]
21
- }
22
- }
23
- ```
24
-
25
- ### 3. Rebuild
26
-
27
- Voice calls require native modules, so you need a **development build** (Expo Go is not supported).
23
+ Then the peer dependencies, which must be installed in **your** app so there is
24
+ exactly one autolinked copy of each native module:
28
25
 
29
26
  ```bash
30
- npx expo prebuild --clean
31
- npx expo run:ios
32
- # or
33
- npx expo run:android
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
34
30
  ```
35
31
 
36
- ## Drop-in widget
37
-
38
- Add a complete voice/text chat widget with one component. It renders a floating action button that opens an animated chat panel.
39
-
40
- ```tsx
41
- import { RinggWidget } from "@ringg/react-native";
42
-
43
- export default function App() {
44
- return (
45
- <RinggWidget
46
- config={{
47
- agentId: "your-agent-id",
48
- xApiKey: "your-api-key",
49
- variables: {
50
- callee_name: "Jane Doe",
51
- },
52
- }}
53
- />
54
- );
55
- }
56
- ```
32
+ Requires React ≥ 18 · React Native ≥ 0.73.
57
33
 
58
- ### Widget props
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/).
59
36
 
60
- | Prop | Type | Description |
61
- | ------------- | -------------------------- | ----------------------------------------------------------- |
62
- | `config` | `RinggWidgetConfig` | Agent ID, API key, variables, and customization (see below) |
63
- | `onEvent` | `(event, payload) => void` | Callback for widget lifecycle events |
64
- | `triggerSize` | `number` | FAB button size in pixels (default: `56`) |
37
+ ## Platform setup (required for voice)
65
38
 
66
- ### Config options
39
+ **Expo** add the LiveKit plugin and the permissions to `app.json`:
67
40
 
68
- ```ts
41
+ ```json
69
42
  {
70
- agentId: string; // Required — your agent ID
71
- xApiKey: string; // Required — your API key
72
- variables: Record<string, string>; // Custom variables sent to the agent
73
- mode?: "prod" | "stage"; // Environment (default: "prod")
74
- title?: string; // Header title
75
- description?: string; // Header description
76
- defaultTab?: "audio" | "text"; // Starting mode (default: "audio")
77
- hideTabSelector?: boolean; // Hide the audio/text toggle
78
- logoUrl?: string; // Custom logo URL
79
- theme?: { // Theme overrides
80
- primaryColor?: string;
81
- backgroundColor?: string;
82
- primaryTextColor?: string;
83
- secondaryTextColor?: string;
84
- buttonStyle?: "rounded" | "pill" | "square";
85
- };
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
+ }
86
60
  }
87
61
  ```
88
62
 
89
- ### Events
90
-
91
- Listen to widget lifecycle events via `onEvent`:
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`.
92
66
 
93
- ```tsx
94
- <RinggWidget
95
- config={config}
96
- onEvent={(event, payload) => {
97
- switch (event) {
98
- case "ringg:conversation_status":
99
- // { status: "started" | "ended", mode: "audio" | "text", callId: string }
100
- break;
101
- case "ringg:widget_status":
102
- // { status: "maximised" | "minimised", mode: "audio" | "text" }
103
- break;
104
- case "ringg:feedback_status":
105
- // { status: "submitted" | "skipped", callId: string, rating?: number }
106
- break;
107
- }
108
- }}
109
- />
110
- ```
67
+ ## Integrate in 3 steps
111
68
 
112
- ## Headless hooks
69
+ ### 1 · Register the WebRTC globals
113
70
 
114
- Build a completely custom UI. Wrap your screen with `RinggProvider`, then use hooks to control calls, chat, and more.
71
+ Once, at your app entry, **before** anything imports LiveKit:
115
72
 
116
- ```tsx
117
- import { RinggProvider, useRinggCall, useRinggChat } from "@ringg/react-native";
118
-
119
- function App() {
120
- return (
121
- <RinggProvider
122
- config={{
123
- agentId: "your-agent-id",
124
- xApiKey: "your-api-key",
125
- variables: {},
126
- }}
127
- >
128
- <ChatScreen />
129
- </RinggProvider>
130
- );
131
- }
73
+ ```ts
74
+ // index.js
75
+ import { registerGlobals } from "@livekit/react-native";
76
+ registerGlobals();
77
+ ```
132
78
 
133
- function ChatScreen() {
134
- const call = useRinggCall();
135
- const chat = useRinggChat();
79
+ ### 2 · Build a transport and a controller
136
80
 
137
- if (!call.isConnected) {
138
- return (
139
- <View>
140
- <Button title="Start voice call" onPress={() => call.startCall("audio")} disabled={call.isLoading} />
141
- <Button title="Start text chat" onPress={() => call.startCall("text")} disabled={call.isLoading} />
142
- </View>
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
+ },
143
113
  );
144
- }
145
-
146
- return (
147
- <View style={{ flex: 1 }}>
148
- <FlatList data={chat.messages} keyExtractor={(item) => String(item.timestamp)} renderItem={({ item }) => <Text style={{ alignSelf: item.isSelf ? "flex-end" : "flex-start" }}>{item.message}</Text>} />
149
-
150
- {call.callMode === "audio" && <Button title={call.isMicEnabled ? "Mute" : "Unmute"} onPress={call.toggleMic} />}
114
+ return { controller, livekit };
115
+ }, []);
151
116
 
152
- {call.callMode === "text" && (
153
- <TextInput
154
- placeholder="Type a message..."
155
- onSubmitEditing={(e) => {
156
- chat.sendMessage(e.nativeEvent.text);
157
- }}
158
- />
159
- )}
117
+ // Releases the microphone and the audio session — do not skip this.
118
+ useEffect(() => () => {
119
+ controller.destroy();
120
+ livekit.dispose();
121
+ }, [controller, livekit]);
160
122
 
161
- <Button title="End call" onPress={call.endCall} />
162
- </View>
163
- );
164
- }
123
+ return <RinggWidget controller={controller} room={livekit.room} />;
124
+ };
165
125
  ```
166
126
 
167
- ### useRinggCall()
168
-
169
- Controls call lifecycle and microphone.
170
-
171
- | Property | Type | Description |
172
- | ----------------- | ------------------------------------------------- | --------------------------------------- |
173
- | `startCall(mode)` | `(mediaType: "audio" \| "text") => Promise<void>` | Start a voice or text session |
174
- | `endCall()` | `() => Promise<void>` | End the current session |
175
- | `isConnected` | `boolean` | Whether the agent is connected |
176
- | `isLoading` | `boolean` | Loading state during connect/disconnect |
177
- | `connectionState` | `"connected" \| "disconnected" \| "reconnecting"` | Detailed connection state |
178
- | `callMode` | `"audio" \| "text"` | Current session mode |
179
- | `callId` | `string \| null` | Current call ID |
180
- | `isMicEnabled` | `boolean` | Microphone state |
181
- | `toggleMic()` | `() => Promise<void>` | Toggle microphone on/off |
182
- | `error` | `string \| null` | Error message if connection failed |
183
- | `clearError()` | `() => void` | Clear the error state |
184
-
185
- ### useRinggChat()
186
-
187
- Send and receive text messages.
188
-
189
- | Property | Type | Description |
190
- | ------------------- | --------------------------------- | ----------------------------------- |
191
- | `messages` | `ChatMessage[]` | All messages in chronological order |
192
- | `sendMessage(text)` | `(text: string) => Promise<void>` | Send a message to the agent |
193
- | `clearMessages()` | `() => void` | Clear message history |
127
+ Passing `room` is optional; it only enables the in-call audio visualizer.
194
128
 
195
- Each `ChatMessage` has: `{ message: string, isSelf: boolean, name: string, timestamp: number }`.
129
+ ### 3 · Mount it over your app
196
130
 
197
- ### useRinggTranscripts()
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:
198
133
 
199
- Real-time speech-to-text during voice calls.
134
+ ```tsx
135
+ <View style={{ flex: 1 }}>
136
+ <YourApp />
137
+ <RinggSupport />
138
+ </View>
139
+ ```
200
140
 
201
- | Property | Type | Description |
202
- | ------------- | ------------------- | --------------------------- |
203
- | `transcripts` | `TranscriptEntry[]` | Live transcription segments |
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:
204
185
 
205
- ### useRinggFeedback()
186
+ ```ts
187
+ import { Platform } from "react-native";
188
+ import { appOrigin, createRinggWidgetController } from "@ringg/react-native";
206
189
 
207
- Post-call feedback collection.
190
+ const BUNDLE_ID = Platform.OS === "android" ? "com.acme.app" : "com.acme.App";
208
191
 
209
- | Property | Type | Description |
210
- | --------------------------------- | ---------------------------------------------------- | ---------------------- |
211
- | `submitFeedback(rating, comment)` | `(rating: number, comment: string) => Promise<void>` | Submit feedback |
212
- | `skipFeedback()` | `() => void` | Skip feedback |
213
- | `isSubmitting` | `boolean` | Submission in progress |
192
+ createRinggWidgetController(
193
+ { agentId: "…", authorization: "Bearer …", clientOrigin: appOrigin(BUNDLE_ID) },
194
+ ports,
195
+ );
196
+ ```
214
197
 
215
- ### useRinggTheme()
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.
216
205
 
217
- Access the resolved theme for custom styling.
206
+ ## Host events
218
207
 
219
- | Property | Type | Description |
220
- | ---------------------- | ----------------------------- | ------------------------------------------ |
221
- | `theme` | `ResolvedWidgetTheme` | Full theme with defaults applied |
222
- | `getSolidColor(color)` | `(color: string) => string` | Extract solid color from gradient |
223
- | `buttonRadius` | `string` | Border radius for the current button style |
224
- | `getContrastText(bg)` | `(bgColor: string) => string` | Get black or white text for a background |
208
+ The controller emits the same events as the web widget. On RN they are
209
+ in-memory rather than DOM events:
225
210
 
226
- ## UI building blocks
211
+ ```ts
212
+ const unsubscribe = controller.eventBus.on("ringg:conversation_status", ({ status, mode, callId }) => {
213
+ analytics.track(`call_${status}`, { mode, callId });
214
+ });
215
+ ```
227
216
 
228
- When using headless hooks, you can still use our pre-built UI components individually:
217
+ Events: `ringg:widget_status`, `ringg:conversation_status`,
218
+ `ringg:feedback_status`, `ringg:calendar_booking`,
219
+ `ringg:component_acknowledgement`.
229
220
 
230
- ```tsx
231
- import { MessageBubble, WidgetHeader, CallControls, ActionButton, FeedbackScreen, TabSelector } from "@ringg/react-native";
232
- ```
221
+ ## Agent-triggered app actions
233
222
 
234
- ## Expo plugin options
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:
235
226
 
236
- ```json
237
- ["@ringg/react-native", { "enableCamera": true }]
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
+ };
238
236
  ```
239
237
 
240
- | Option | Type | Default | Description |
241
- | -------------- | --------- | ------- | ---------------------------------------------------- |
242
- | `enableCamera` | `boolean` | `false` | Request camera permission (for future video support) |
238
+ ## Notification sound
243
239
 
244
- ## Bare React Native
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:
245
243
 
246
- If you're not using Expo, see the [bare React Native setup guide](https://ringg.ai/docs/react-native/bare) for manual native configuration.
244
+ ```ts
245
+ import { createAudioPlayer } from "expo-audio";
246
+ import { createNotificationPlayer } from "@ringg/react-native";
247
+ import { DEFAULT_CONFIG } from "@ringg/core";
247
248
 
248
- ## License
249
+ const notification = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
250
+ ```
249
251
 
250
- [MIT](./LICENSE)
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.