@ringg/react-native 0.3.0 → 0.4.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 +90 -80
- package/app.plugin.js +73 -0
- package/dist/index.d.mts +267 -2
- package/dist/index.d.ts +267 -2
- package/dist/index.js +825 -412
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +797 -431
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ views are native.
|
|
|
20
20
|
npm install @ringg/react-native
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Then the
|
|
24
|
-
|
|
23
|
+
Then the three native modules. React Native autolinking only wires native code
|
|
24
|
+
that resolves from **your** app's `node_modules`, so these cannot be ours:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npx expo install @livekit/react-native @livekit/react-native-webrtc
|
|
27
|
+
npx expo install @livekit/react-native @livekit/react-native-webrtc react-native-svg
|
|
28
28
|
# or, without Expo:
|
|
29
|
-
npm install @livekit/react-native @livekit/react-native-webrtc
|
|
29
|
+
npm install @livekit/react-native @livekit/react-native-webrtc react-native-svg
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Requires React ≥ 18 · React Native ≥ 0.73.
|
|
@@ -36,109 +36,120 @@ Requires React ≥ 18 · React Native ≥ 0.73.
|
|
|
36
36
|
|
|
37
37
|
## Platform setup (required for voice)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Voice calls need a microphone, background audio and LiveKit's manifest
|
|
40
|
+
metadata. Text chat needs none of it.
|
|
41
|
+
|
|
42
|
+
**Expo** — add the plugin to `app.json` and rebuild. It applies all of the
|
|
43
|
+
above, including the native transport's own configuration:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"expo": {
|
|
48
|
+
"plugins": ["@ringg/react-native"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To change the microphone prompt your users see:
|
|
40
54
|
|
|
41
55
|
```json
|
|
42
56
|
{
|
|
43
57
|
"expo": {
|
|
44
|
-
"plugins": ["@
|
|
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
|
-
}
|
|
58
|
+
"plugins": [["@ringg/react-native", { "microphonePermissionText": "Talk to support." }]]
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
**Bare React Native** — the same
|
|
64
|
-
and `UIBackgroundModes: [audio]` in `ios/<App>/Info.plist`, and the four
|
|
65
|
-
permissions above in `android/app/src/main/AndroidManifest.xml`.
|
|
63
|
+
**Bare React Native** — no config plugins, so the same values by hand.
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
`ios/<App>/Info.plist`:
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
```xml
|
|
68
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
69
|
+
<string>Voice calls use the microphone.</string>
|
|
70
|
+
<key>UIBackgroundModes</key>
|
|
71
|
+
<array><string>audio</string></array>
|
|
72
|
+
```
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
`android/app/src/main/AndroidManifest.xml`:
|
|
72
75
|
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
```xml
|
|
77
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
78
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
79
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
80
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
81
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
77
82
|
```
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
## Integrate
|
|
80
85
|
|
|
81
86
|
```tsx
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
};
|
|
87
|
+
import { RinggWidget, appOrigin } from "@ringg/react-native";
|
|
88
|
+
import { Platform } from "react-native";
|
|
89
|
+
|
|
90
|
+
const BUNDLE_ID = Platform.OS === "android" ? "com.acme.app" : "com.acme.App";
|
|
96
91
|
|
|
97
|
-
export const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
92
|
+
export const App = () => (
|
|
93
|
+
<View style={{ flex: 1 }}>
|
|
94
|
+
<YourApp />
|
|
95
|
+
<RinggWidget
|
|
96
|
+
config={{
|
|
102
97
|
agentId: "<your-agent-id>",
|
|
103
98
|
authorization: "Bearer <your-token>",
|
|
99
|
+
clientOrigin: appOrigin(BUNDLE_ID),
|
|
104
100
|
title: "Support",
|
|
105
|
-
description: "How can we help?",
|
|
106
101
|
defaultTab: "text", // or "audio"
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
};
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
125
106
|
```
|
|
126
107
|
|
|
127
|
-
|
|
108
|
+
That is the whole integration. The widget renders its own floating trigger and
|
|
109
|
+
panel over whatever is behind it, so make it the **last child** of your root
|
|
110
|
+
view. Tap the trigger and the chat or voice panel opens.
|
|
111
|
+
|
|
112
|
+
Endpoints, the media transport and the microphone port are the package's
|
|
113
|
+
business, not yours. `config.mode` (`dev` / `stage` / `prod`, default `prod`)
|
|
114
|
+
picks the environment.
|
|
115
|
+
|
|
116
|
+
### Reading events, or driving the panel yourself
|
|
117
|
+
|
|
118
|
+
`onReady` hands back the controller the widget built:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
<RinggWidget config={config} onReady={(controller) => {
|
|
122
|
+
controller.eventBus.on("ringg:conversation_status", ({ status, callId }) => {
|
|
123
|
+
analytics.track(`call_${status}`, { callId });
|
|
124
|
+
});
|
|
125
|
+
}} />
|
|
126
|
+
```
|
|
128
127
|
|
|
129
|
-
###
|
|
128
|
+
### Owning the lifecycle yourself
|
|
130
129
|
|
|
131
|
-
`
|
|
132
|
-
|
|
130
|
+
Pass `ports` to override anything the widget would wire by default — a mock
|
|
131
|
+
transport in tests, your own `urlResolver`, a notification player, a handler for
|
|
132
|
+
agent-triggered app actions:
|
|
133
133
|
|
|
134
134
|
```tsx
|
|
135
|
-
<
|
|
136
|
-
<YourApp />
|
|
137
|
-
<RinggSupport />
|
|
138
|
-
</View>
|
|
135
|
+
<RinggWidget config={config} ports={{ onDomAction: dispatcher }} />
|
|
139
136
|
```
|
|
140
137
|
|
|
141
|
-
|
|
138
|
+
Or build the controller yourself and pass it instead of `config`, when the
|
|
139
|
+
widget cannot own the transport (a shared room, a custom adapter, several views
|
|
140
|
+
on one controller):
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const livekit = createLiveKitTransport();
|
|
144
|
+
const controller = createRinggWidgetController(config, {
|
|
145
|
+
transport: livekit.transport,
|
|
146
|
+
urlResolver: defaultUrlResolver,
|
|
147
|
+
micPermission: createNativeMicPermission(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
<RinggWidget controller={controller} room={livekit.room} />;
|
|
151
|
+
// You built it, so you destroy it: controller.destroy(); livekit.dispose();
|
|
152
|
+
```
|
|
142
153
|
|
|
143
154
|
## Configuration
|
|
144
155
|
|
|
@@ -243,8 +254,7 @@ every integrator. Wire whichever player your app already has:
|
|
|
243
254
|
|
|
244
255
|
```ts
|
|
245
256
|
import { createAudioPlayer } from "expo-audio";
|
|
246
|
-
import { createNotificationPlayer } from "@ringg/react-native";
|
|
247
|
-
import { DEFAULT_CONFIG } from "@ringg/core";
|
|
257
|
+
import { createNotificationPlayer, DEFAULT_CONFIG } from "@ringg/react-native";
|
|
248
258
|
|
|
249
259
|
const notification = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
|
|
250
260
|
```
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo config plugin — the whole native setup, in one entry.
|
|
3
|
+
*
|
|
4
|
+
* Voice calls need a microphone, background audio and the LiveKit native
|
|
5
|
+
* module's own manifest metadata. Making integrators assemble that themselves
|
|
6
|
+
* meant three separate things in their `app.json`: our transport's plugin, a
|
|
7
|
+
* list of Android permissions, and two `Info.plist` keys. Those are our
|
|
8
|
+
* requirements, so they are our job:
|
|
9
|
+
*
|
|
10
|
+
* { "expo": { "plugins": ["@ringg/react-native"] } }
|
|
11
|
+
*
|
|
12
|
+
* Bare React Native apps have no config plugins and still edit `Info.plist`
|
|
13
|
+
* and `AndroidManifest.xml` by hand; the values are in the README.
|
|
14
|
+
*
|
|
15
|
+
* Plain JS on purpose: Expo resolves `app.plugin.js` from the package root
|
|
16
|
+
* before any bundler runs, so it cannot be part of the tsup build.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { AndroidConfig, withInfoPlist, withPlugins } = require("@expo/config-plugins");
|
|
20
|
+
|
|
21
|
+
// The subpath, not the package: `main` is deliberately empty there, and the
|
|
22
|
+
// plugin lives at the `app.plugin.js` entry Expo looks for by convention.
|
|
23
|
+
// Requiring it from here (rather than passing the package name for Expo to
|
|
24
|
+
// resolve) keeps it working under pnpm and Yarn PnP, where a dependency of a
|
|
25
|
+
// dependency is not resolvable from the app root.
|
|
26
|
+
const liveKitPlugin = require("@livekit/react-native-expo-plugin/app.plugin");
|
|
27
|
+
const withLiveKit = liveKitPlugin.default ?? liveKitPlugin;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bluetooth is listed without `maxSdkVersion` filtering: Expo's permission
|
|
31
|
+
* helper writes plain `uses-permission` entries, and the legacy-only variants
|
|
32
|
+
* (`BLUETOOTH`, `BLUETOOTH_ADMIN`) are harmless on modern Android. Apps that
|
|
33
|
+
* care can strip them with their own `withAndroidManifest` plugin.
|
|
34
|
+
*/
|
|
35
|
+
const ANDROID_PERMISSIONS = [
|
|
36
|
+
"android.permission.INTERNET",
|
|
37
|
+
"android.permission.RECORD_AUDIO",
|
|
38
|
+
"android.permission.MODIFY_AUDIO_SETTINGS",
|
|
39
|
+
"android.permission.ACCESS_NETWORK_STATE",
|
|
40
|
+
"android.permission.BLUETOOTH_CONNECT",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MICROPHONE_TEXT = "Voice calls with the assistant use the microphone.";
|
|
44
|
+
|
|
45
|
+
/** iOS: the permission prompt copy, and the background mode a live call needs. */
|
|
46
|
+
const withRinggIos = (config, props) =>
|
|
47
|
+
withInfoPlist(config, (iosConfig) => {
|
|
48
|
+
// An app that already declares its own copy keeps it: the prompt is user
|
|
49
|
+
// facing, and ours is a fallback rather than a correction.
|
|
50
|
+
iosConfig.modResults.NSMicrophoneUsageDescription = props.microphonePermissionText ?? iosConfig.modResults.NSMicrophoneUsageDescription ?? DEFAULT_MICROPHONE_TEXT;
|
|
51
|
+
|
|
52
|
+
const backgroundModes = new Set(iosConfig.modResults.UIBackgroundModes ?? []);
|
|
53
|
+
backgroundModes.add("audio");
|
|
54
|
+
iosConfig.modResults.UIBackgroundModes = Array.from(backgroundModes);
|
|
55
|
+
|
|
56
|
+
return iosConfig;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} config Expo config
|
|
61
|
+
* @param {{ microphonePermissionText?: string }} [props]
|
|
62
|
+
*/
|
|
63
|
+
const withRingg = (config, props = {}) =>
|
|
64
|
+
withPlugins(config, [
|
|
65
|
+
// `communication` routes audio to the earpiece/Bluetooth like a phone call
|
|
66
|
+
// rather than the media stream. A call played through the media channel is
|
|
67
|
+
// the wrong volume slider and the wrong speaker.
|
|
68
|
+
[withLiveKit, { android: { audioType: "communication" } }],
|
|
69
|
+
[AndroidConfig.Permissions.withPermissions, ANDROID_PERMISSIONS],
|
|
70
|
+
[withRinggIos, props],
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
module.exports = withRingg;
|