@dialstack/sdk-native 0.1.0 → 0.2.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 CHANGED
@@ -8,28 +8,10 @@ pieces (`<DialPad>` / `<IncomingCall>` / `<OngoingCall>`).
8
8
  import { Softphone, SoftphoneProvider } from '@dialstack/sdk-native';
9
9
  ```
10
10
 
11
- ## Why a separate package (not `@dialstack/sdk/native`)
12
-
13
- This is a distinct package from `@dialstack/sdk` **on purpose**: it declares the
14
- React Native peer dependencies. Keeping them here means the web SDK's dependency
15
- graph is _structurally_ free of anything React Native — a web app that installs
16
- `@dialstack/sdk` can never pull `react-native-webrtc`/`-svg`/`-incall-manager`.
17
-
18
- `@dialstack/sdk-native` is self-contained: it inlines its own compiled copy of
19
- the shared headless calling core and call-state hooks at build time, so it has
20
- **no runtime dependency on `@dialstack/sdk`** (the core is authored once in that
21
- package and shared at build time only). The core is written to the standard
22
- browser WebRTC surface (`RTCPeerConnection`, `MediaStream`, `navigator.mediaDevices`);
23
- call `registerGlobals()` from your WebRTC package at your app's entry point so
24
- that surface exists on React Native before the SDK runs. The two RN-only gaps —
25
- outbound ringback audio and E911 persistence — are supplied by the softphone
26
- provider (an InCallManager-backed ringback and your `storage` adapter).
27
-
28
- ```js
29
- // index.js — before anything imports the SDK
30
- import { registerGlobals } from 'react-native-webrtc';
31
- registerGlobals();
32
- ```
11
+ React components for the web live in
12
+ [`@dialstack/sdk-react`](https://www.npmjs.com/package/@dialstack/sdk-react).
13
+ This package is self-contained: install it alone, and nothing from the web SDK
14
+ comes with it.
33
15
 
34
16
  ## Install
35
17
 
@@ -52,8 +34,99 @@ globals `registerGlobals()` installs, so any spec-compliant build works. The
52
34
  `react-native-webrtc` peer is **optional** precisely so a differently-named
53
35
  package can satisfy it without npm installing a second copy alongside.
54
36
 
55
- See the example apps under `../mobile/` for a runnable Expo app and a bare React
56
- Native app.
37
+ > **Installing our fork from GitHub:** its entry points are generated by its
38
+ > `prepare` script at install time. If your npm or CI blocks install scripts,
39
+ > the package installs without them and the failure surfaces later as
40
+ > `Cannot find module`, not as an install error — allow install scripts for
41
+ > that dependency.
42
+
43
+ ### Install the WebRTC globals
44
+
45
+ The SDK core is written to the standard browser WebRTC surface
46
+ (`RTCPeerConnection`, `MediaStream`, `navigator.mediaDevices`), so call
47
+ `registerGlobals()` from your WebRTC package at your app's entry point, before
48
+ anything imports the SDK:
49
+
50
+ ```js
51
+ // index.js
52
+ import { registerGlobals } from 'react-native-webrtc';
53
+ registerGlobals();
54
+ ```
55
+
56
+ Two RN-only gaps — outbound ringback audio and E911 persistence — are supplied
57
+ by the softphone provider (an InCallManager-backed ringback and your `storage`
58
+ adapter).
59
+
60
+ ## Permissions (required)
61
+
62
+ **Without microphone permission a call fails with `Call failed` and no other
63
+ symptom** — `getUserMedia` never runs, so there is nothing else to see. Declare
64
+ these before your first call.
65
+
66
+ ### Expo
67
+
68
+ ```json
69
+ {
70
+ "expo": {
71
+ "plugins": ["@config-plugins/react-native-webrtc"],
72
+ "android": {
73
+ "permissions": [
74
+ "android.permission.RECORD_AUDIO",
75
+ "android.permission.MODIFY_AUDIO_SETTINGS",
76
+ "android.permission.INTERNET",
77
+ "android.permission.ACCESS_NETWORK_STATE",
78
+ "android.permission.VIBRATE"
79
+ ]
80
+ },
81
+ "ios": {
82
+ "infoPlist": {
83
+ "NSMicrophoneUsageDescription": "… needs the microphone to place and receive calls.",
84
+ "UIBackgroundModes": ["audio", "voip"]
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ `react-native-webrtc` is a native module, so it needs a **development build** —
92
+ it cannot run in Expo Go. Re-run `expo prebuild` after changing any of the above.
93
+
94
+ The config plugin also injects permissions of its own that this SDK does not use
95
+ — including `CAMERA`, `SYSTEM_ALERT_WINDOW` and `BLUETOOTH` — and writes a
96
+ default camera-usage string. If a store listing or privacy review asks about
97
+ them, drop the ones you don't need with Expo's
98
+ [`android.blockedPermissions`](https://docs.expo.dev/versions/latest/config/app/#blockedpermissions).
99
+
100
+ ### Bare React Native
101
+
102
+ `android/app/src/main/AndroidManifest.xml`:
103
+
104
+ ```xml
105
+ <uses-permission android:name="android.permission.INTERNET" />
106
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
107
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
108
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
109
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
110
+ <uses-permission android:name="android.permission.VIBRATE" />
111
+ ```
112
+
113
+ `VIBRATE` is what gives the incoming call its haptic buzz: Android enforces it
114
+ by throwing rather than no-op'ing, so without it the call still rings but never
115
+ vibrates. The SDK catches that throw, so omitting the permission degrades
116
+ gracefully — it does not break the call. Neither `react-native` nor
117
+ `react-native-incall-manager` declares it, so your app must.
118
+
119
+ `ios/<YourApp>/Info.plist`:
120
+
121
+ ```xml
122
+ <key>NSMicrophoneUsageDescription</key>
123
+ <string>… needs the microphone to place and receive calls.</string>
124
+ <key>UIBackgroundModes</key>
125
+ <array>
126
+ <string>audio</string>
127
+ <string>voip</string>
128
+ </array>
129
+ ```
57
130
 
58
131
  ## DTMF
59
132
 
@@ -68,7 +141,7 @@ npm install 'github:dialstack/react-native-webrtc#7e9a0eb55e068b2ba452f22e02e4b7
68
141
  With any other WebRTC package — including LiveKit's and Stream's forks, neither
69
142
  of which exposes `RTCRtpSender.dtmf` — `Call.canSendDtmf` is `false`, the in-call
70
143
  keypad stays hidden, `Call.sendDtmf()` throws `call_failed`, and the SDK logs
71
- `[dialstack] DTMF is unavailable …` on the first call. Calls are otherwise
144
+ `[dialstack] DTMF is unavailable …` once per app process. Calls are otherwise
72
145
  unaffected, but there is no way to navigate an IVR or phone tree. That is the
73
146
  tradeoff to weigh if another library forces a particular WebRTC package on you.
74
147
 
@@ -96,20 +169,77 @@ const storage: PlatformStorage = {
96
169
  </SoftphoneProvider>;
97
170
  ```
98
171
 
99
- The Expo `../examples/mobile/expo` app ships this MMKV adapter; the bare
100
- `../examples/mobile/bare` app ships an AsyncStorage one. Either works —
101
172
  `storage` just needs `getItem`/`setItem`/`removeItem`.
102
173
 
174
+ ## Call audio
175
+
176
+ By default `<SoftphoneProvider>` owns call audio through
177
+ `react-native-incall-manager`: it rings inbound calls, plays the outbound
178
+ ringback, and holds the audio session (which also turns on the proximity sensor)
179
+ while a call is connected. The ringback goes only to a phone the provider creates
180
+ from `token`: a phone passed as `existingPhone` keeps the `ringback` it was
181
+ created with, so pass one to `createPhone({ ringback })` yourself.
182
+
183
+ When the operating system owns the call instead, because you report calls to
184
+ CallKit or Telecom, the provider must stay out of it: the OS rings the phone and
185
+ activates the session itself, and ringing on top of it costs the answered call
186
+ its microphone. Set `callAudio="host"` and the provider does none of the above.
187
+ With a `bridge` it is always `"host"`. Anything you still want, such as the proximity
188
+ sensor, is then yours to drive (`InCallManager.startProximitySensor()` does not
189
+ touch the audio session).
190
+
191
+ The output route is yours too. Without an `audioOutput` controller the in-call
192
+ speaker button is disabled. To enable it, pass one built over your OS integration:
193
+
194
+ ```tsx
195
+ const audioOutput: AudioOutputController = {
196
+ current: () => lastReportedRoute, // what the OS reports, same object until it changes
197
+ onChange: (listener) => subscribeToRouteChanges(listener),
198
+ setSpeaker: (on) => requestSpeaker(on),
199
+ };
200
+
201
+ <SoftphoneProvider bridge={bridge} audioOutput={audioOutput}>
202
+ …
203
+ </SoftphoneProvider>;
204
+ ```
205
+
206
+ The button shows only the route the OS reports. A switch made from the CallKit
207
+ or Telecom UI, a headset, or a car therefore shows up in-app as well.
208
+ `useAudioOutput()` exposes the same state if you build your own UI.
209
+
210
+ ## Example apps
211
+
212
+ Three runnable apps, all wired with the permissions and plugin above:
213
+
214
+ - [Expo](https://github.com/dialstack/dialstack-sdk/tree/main/examples/mobile/expo)
215
+ — ships the MMKV storage adapter
216
+ - [Bare React Native](https://github.com/dialstack/dialstack-sdk/tree/main/examples/mobile/bare)
217
+ — ships an AsyncStorage adapter
218
+ - [OS integration](https://github.com/dialstack/dialstack-sdk/tree/main/examples/mobile/os-integration)
219
+ — the worked example for the OS call surface: calls on a locked or killed
220
+ phone via push wake, `OsCallAdapter` over expo-callkit-telecom. Built and
221
+ verified on both Android (FCM + Telecom) and iOS (PushKit + CallKit).
222
+
103
223
  ## Scope
104
224
 
105
- This package provides the in-app calling surface: the provider, the call state,
106
- and the React Native UI. The operating system's own call experience — the
107
- full-screen incoming call, ringing on a locked screen, and waking the app from a
108
- push — is the integrator's to wire, using the platform frameworks directly (iOS
109
- PushKit + CallKit, Android FCM + Telecom/ConnectionService).
225
+ This package provides the in-app calling surface — the provider, the call state,
226
+ and the React Native UI — plus the bridge between the SDK and the operating
227
+ system's own call experience.
228
+
229
+ Binding the two remains the integrator's work, but not from scratch:
230
+ `NativeCallBridge` keeps an OS call session and an SDK `Call` in step in both
231
+ directions, and `OsCallAdapter` is the interface it drives, so the part you write
232
+ is one adapter over your chosen call library rather than the whole state machine.
233
+ A contract suite in `@dialstack/sdk-native/testing` exercises an adapter against
234
+ the behaviours the bridge depends on.
235
+
236
+ What the package does not ship is the platform wiring itself: the native call
237
+ library (iOS PushKit + CallKit, Android FCM + Telecom/ConnectionService) and the
238
+ push delivery. DialStack stores no device tokens — arranging the push that wakes
239
+ your app is yours. The OS-integration example above is a complete worked wiring
240
+ of both on Android.
110
241
 
111
- The seams for that are public. `useSoftphone()` exposes `calls`,
242
+ The lower-level seams stay public too: `useSoftphone()` exposes `calls`,
112
243
  `incomingCalls`, `activeCall` and `actions.callActionsFor(call)`, and every
113
- `Call` carries a stable `id` and `state`, so a native call session can be
114
- created, kept in step, and torn down alongside the SDK's own. DialStack stores
115
- no device tokens — delivering the push that wakes your app is yours to arrange.
244
+ `Call` carries a stable `id` and `state`, so an integrator who wants to drive the
245
+ OS session directly can still do so.