@telnyx/react-voice-commons-sdk 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/CHANGELOG.md +21 -0
- package/README.md +108 -11
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +5 -4
- package/lib/internal/calls/call-state-controller.d.ts +7 -0
- package/lib/internal/calls/call-state-controller.js +23 -0
- package/lib/internal/session/session-manager.d.ts +14 -1
- package/lib/internal/session/session-manager.js +23 -2
- package/lib/models/call.d.ts +12 -0
- package/lib/models/call.js +22 -0
- package/lib/telnyx-voip-client.js +5 -0
- package/package.json +6 -5
- package/src/internal/calls/call-state-controller.ts +26 -0
- package/src/internal/session/session-manager.ts +26 -3
- package/src/models/call.ts +24 -0
- package/src/telnyx-voip-client.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# CHANGELOG.md
|
|
2
2
|
|
|
3
|
+
## [0.4.0] (2026-04-19)
|
|
4
|
+
|
|
5
|
+
### Enhancement
|
|
6
|
+
|
|
7
|
+
- **DTMF support.** Added `Call.dtmf(digits: string): Promise<void>` — sends DTMF tones on the current call as Verto `INFO` messages. Accepts a single digit (e.g. `"5"`) or a whole string (e.g. `"1234#"`). Valid characters are `0-9`, `A-D`, `*`, and `#`; anything else is silently dropped by the underlying SDK. Call state must be `ACTIVE` — will throw otherwise.
|
|
8
|
+
|
|
9
|
+
### Bug Fixing
|
|
10
|
+
|
|
11
|
+
- **Fixed `IllegalArgumentException: Could not find @ReactModule annotation in VoicePnBridgeModule` crash** on Android when the app is cold-started by a push notification action (Answer/Decline). React Native 0.79+ requires `@ReactModule(name = …)` on native modules that are looked up by class via `ReactContext.getNativeModule(Class)`. The annotation has been added to `VoicePnBridgeModule`, and the module name is now sourced from a single `NAME` constant.
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
|
|
15
|
+
- Now requires `@telnyx/react-native-voice-sdk >= 0.4.2`, which fixes a bug where every `Call.dtmf()` invocation threw `Invalid DTMF response received` even though the tone was delivered. See the [voice-sdk 0.4.2 changelog](../package/CHANGELOG.md#042-2026-04-19) for details.
|
|
16
|
+
|
|
17
|
+
## [0.3.1] (2026-04-16)
|
|
18
|
+
|
|
19
|
+
### Bug Fixing
|
|
20
|
+
|
|
21
|
+
- Fixed stale `CONNECTED` state during background disconnect: `SessionManager.disconnect()` now emits `DISCONNECTED` before awaiting the underlying client teardown. Previously, observers (including the auto-reconnect logic in `TelnyxVoiceApp`) could read a stale `CONNECTED` value while the socket was being torn down, causing auto-reconnection to be skipped and subsequent `newCall()` attempts to fail with `Cannot make call when connection state is: DISCONNECTED` or `No connection exists. Please connect first.`
|
|
22
|
+
- Tracked calls are now cleared on disconnect. Previously, calls left in non-terminal states when the socket was torn down would accumulate across background/foreground cycles, since a dead socket never emits the `ENDED`/`FAILED` events that normally trigger per-call cleanup.
|
|
23
|
+
|
|
3
24
|
## [0.3.0] (2026-04-15)
|
|
4
25
|
|
|
5
26
|
### ⚠️ Breaking changes
|
package/README.md
CHANGED
|
@@ -118,9 +118,18 @@ call.callState$.subscribe((state) => {
|
|
|
118
118
|
|
|
119
119
|
### Navigation
|
|
120
120
|
|
|
121
|
-
As of **v0.3.0**, the SDK no longer navigates the host app. Routing on state transitions (e.g. redirecting to a login screen on disconnect, surfacing
|
|
121
|
+
As of **v0.3.0**, the SDK no longer navigates the host app. Routing on state transitions (e.g. redirecting to a login screen on disconnect, surfacing an in-call screen when a call arrives via push) is entirely the host app's responsibility. Subscribe to the observables below and invoke your own navigator.
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
**What to observe:**
|
|
124
|
+
|
|
125
|
+
| Observable | Emits | Use it for |
|
|
126
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `voipClient.connectionState$` | `TelnyxConnectionState` (`CONNECTING`, `CONNECTED`, `RECONNECTING`, `DISCONNECTED`, `ERROR`) | Redirect to login on `DISCONNECTED`; gate outbound-call UI on `CONNECTED`. There is no separate `loginState$` — `CONNECTED` means the socket is up **and** authenticated. |
|
|
128
|
+
| `voipClient.activeCall$` | `Call \| null` | Navigate to your in-call screen when a call appears. Primary signal for push-launched cold starts — when the SDK finishes the push-driven login and the call arrives, this emits. |
|
|
129
|
+
| `voipClient.calls$` | `Call[]` | Multi-call UIs (call waiting, conference). |
|
|
130
|
+
| `call.callState$` | `TelnyxCallState` | Per-call transitions (ringing / active / held / ended). |
|
|
131
|
+
|
|
132
|
+
#### Redirect to login on disconnect
|
|
124
133
|
|
|
125
134
|
```tsx
|
|
126
135
|
import { router } from 'expo-router';
|
|
@@ -137,6 +146,43 @@ useEffect(() => {
|
|
|
137
146
|
}, []);
|
|
138
147
|
```
|
|
139
148
|
|
|
149
|
+
#### Navigate to in-call screen when a call arrives (push-launched or otherwise)
|
|
150
|
+
|
|
151
|
+
This is the piece that pairs with the push flow: after `isLaunchedFromPushNotification()` tells you to skip manual login, the SDK drives login internally and the call shows up on `activeCall$`. The same subscription handles foreground pushes and outbound calls, so you only need one:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { router } from 'expo-router';
|
|
155
|
+
import { useEffect } from 'react';
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
// Handle the cold-start race: if the call was already delivered before this
|
|
159
|
+
// component mounted (common on push-launched cold starts), read it synchronously.
|
|
160
|
+
if (voipClient.currentActiveCall) {
|
|
161
|
+
router.replace('/call');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sub = voipClient.activeCall$.subscribe((call) => {
|
|
165
|
+
if (call) router.replace('/call');
|
|
166
|
+
else router.replace('/dialer');
|
|
167
|
+
});
|
|
168
|
+
return () => sub.unsubscribe();
|
|
169
|
+
}, []);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Note on CallKit / ConnectionService:** the native call UI (ringtone, answer/decline) is shown by the OS regardless — your RN screen is only visible once the user taps into the app. If you only need native UI, you can skip the navigation step entirely.
|
|
173
|
+
|
|
174
|
+
#### Optional: gate outbound-call UI on CONNECTED
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { canMakeCalls } from '@telnyx/react-voice-commons-sdk';
|
|
178
|
+
|
|
179
|
+
const [canCall, setCanCall] = useState(false);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const sub = voipClient.connectionState$.subscribe((s) => setCanCall(canMakeCalls(s)));
|
|
182
|
+
return () => sub.unsubscribe();
|
|
183
|
+
}, []);
|
|
184
|
+
```
|
|
185
|
+
|
|
140
186
|
The same pattern works with `react-navigation`, React Router, or any other navigator — the SDK is agnostic.
|
|
141
187
|
|
|
142
188
|
### 4. Call Management
|
|
@@ -152,8 +198,64 @@ await call.answer();
|
|
|
152
198
|
await call.mute();
|
|
153
199
|
await call.hold();
|
|
154
200
|
await call.hangup();
|
|
201
|
+
|
|
202
|
+
// Send DTMF tones (for IVRs, conference pins, etc.)
|
|
203
|
+
await call.dtmf('1'); // single digit
|
|
204
|
+
await call.dtmf('1234#'); // whole string
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**DTMF:** `call.dtmf(digits)` sends each character as a Verto `INFO` message to the Telnyx platform. Valid characters are `0-9`, `A-D`, `*`, and `#`; any other characters are silently dropped by the underlying SDK. The call must be in the `ACTIVE` state — calling `dtmf()` in any other state throws. Safe to call with a single digit for dialpad presses or with a whole string for pre-recorded sequences.
|
|
208
|
+
|
|
209
|
+
### Push Notification Flow
|
|
210
|
+
|
|
211
|
+
You do not wire push handlers in JS. The native layer does the work:
|
|
212
|
+
|
|
213
|
+
- **Android**: `TelnyxFirebaseMessagingService` receives the FCM message, posts the call notification, and launches `TelnyxMainActivity` with the intent on Answer/Decline.
|
|
214
|
+
- **iOS**: `TelnyxVoipPushHandler` receives the PushKit payload and reports the call to CallKit.
|
|
215
|
+
|
|
216
|
+
The SDK then connects the socket and restores the call internally. You observe `voipClient.calls$` to render UI.
|
|
217
|
+
|
|
218
|
+
#### Detecting a Push-Launched Cold Start
|
|
219
|
+
|
|
220
|
+
When the OS wakes the app from a terminated state to deliver a call, the SDK is already handling login via the push flow. If your app _also_ triggers a login on mount, you get two competing sessions (double-login), which causes the call to fail or the socket to churn.
|
|
221
|
+
|
|
222
|
+
Use the static `TelnyxVoipClient.isLaunchedFromPushNotification()` to guard your login:
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { TelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
|
|
226
|
+
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
|
|
229
|
+
if (isFromPush) return; // SDK is handling login via the push flow
|
|
230
|
+
voipClient.loginFromStoredConfig(); // Normal cold start
|
|
231
|
+
});
|
|
232
|
+
}, []);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Returns `true` when a pending FCM intent (Android) or PushKit payload (iOS) has not yet been consumed.
|
|
236
|
+
|
|
237
|
+
#### Common Mistake: Manual or Automatic Login on Push
|
|
238
|
+
|
|
239
|
+
The single most common integration bug is re-logging in while the SDK is already handling a push:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// WRONG — runs on every mount, including push-launched cold starts
|
|
243
|
+
React.useEffect(() => {
|
|
244
|
+
voipClient.login(config); // or loginFromStoredConfig(), or loginWithToken()
|
|
245
|
+
}, []);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
// Right — guard the login on push-launched cold starts
|
|
250
|
+
React.useEffect(() => {
|
|
251
|
+
TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
|
|
252
|
+
if (!isFromPush) voipClient.loginFromStoredConfig();
|
|
253
|
+
});
|
|
254
|
+
}, []);
|
|
155
255
|
```
|
|
156
256
|
|
|
257
|
+
Symptoms of getting this wrong: the incoming call rings briefly then disappears, the socket disconnects mid-call, or CallKit shows a call that immediately ends.
|
|
258
|
+
|
|
157
259
|
### Authentication & Persistent Storage
|
|
158
260
|
|
|
159
261
|
The library supports both credential-based and token-based authentication with automatic persistence for seamless reconnection.
|
|
@@ -273,17 +375,12 @@ The `TelnyxMainActivity` provides:
|
|
|
273
375
|
|
|
274
376
|
### 2. Push Notification Setup
|
|
275
377
|
|
|
276
|
-
1. Place `google-services.json` in the project root
|
|
277
|
-
2.
|
|
378
|
+
1. Place `google-services.json` in the project root.
|
|
379
|
+
2. Create an FCM service that extends `TelnyxFirebaseMessagingService` and a notification action receiver that extends `TelnyxNotificationActionReceiver`, then register both in `AndroidManifest.xml`. See the [push notification app setup guide](../docs-markdown/push-notification/app-setup.md) for the full boilerplate.
|
|
278
380
|
|
|
279
|
-
|
|
280
|
-
import messaging from '@react-native-firebase/messaging';
|
|
281
|
-
import { TelnyxVoiceApp } from '@telnyx/react-voice-commons-sdk';
|
|
381
|
+
> **Do not** register `messaging().setBackgroundMessageHandler(...)` from JS. Android push is handled entirely by the native `TelnyxFirebaseMessagingService` — adding a JS handler will fight the native layer and can double-process calls. There is no `TelnyxVoiceApp.handleBackgroundPush` step required on Android.
|
|
282
382
|
|
|
283
|
-
|
|
284
|
-
await TelnyxVoiceApp.handleBackgroundPush(remoteMessage.data);
|
|
285
|
-
});
|
|
286
|
-
```
|
|
383
|
+
See [Push Notification Flow](#push-notification-flow) below for how to detect push-launched cold starts and avoid double-login.
|
|
287
384
|
|
|
288
385
|
### iOS Integration
|
|
289
386
|
|
|
@@ -9,17 +9,18 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
9
9
|
import com.facebook.react.bridge.Promise
|
|
10
10
|
import com.facebook.react.bridge.WritableMap
|
|
11
11
|
import com.facebook.react.bridge.Arguments
|
|
12
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
12
13
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
13
14
|
|
|
15
|
+
@ReactModule(name = VoicePnBridgeModule.NAME)
|
|
14
16
|
class VoicePnBridgeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
15
17
|
|
|
16
18
|
companion object {
|
|
19
|
+
const val NAME = "VoicePnBridge"
|
|
17
20
|
private const val TAG = "VoicePnBridgeModule"
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
override fun getName(): String
|
|
21
|
-
return "VoicePnBridge"
|
|
22
|
-
}
|
|
22
|
+
|
|
23
|
+
override fun getName(): String = NAME
|
|
23
24
|
|
|
24
25
|
@ReactMethod
|
|
25
26
|
fun getPendingPushAction(promise: Promise) {
|
|
@@ -71,6 +71,13 @@ export declare class CallStateController {
|
|
|
71
71
|
isWaitingForInvite: () => boolean;
|
|
72
72
|
onInviteAutoAccepted: () => void;
|
|
73
73
|
}): void;
|
|
74
|
+
/**
|
|
75
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
76
|
+
* calls left in non-terminal states (because the socket died before
|
|
77
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
78
|
+
* across reconnect cycles.
|
|
79
|
+
*/
|
|
80
|
+
clearAllCalls(): void;
|
|
74
81
|
/**
|
|
75
82
|
* Dispose of the controller and clean up resources
|
|
76
83
|
*/
|
|
@@ -161,6 +161,29 @@ class CallStateController {
|
|
|
161
161
|
this._isWaitingForInvite = callbacks.isWaitingForInvite;
|
|
162
162
|
this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
166
|
+
* calls left in non-terminal states (because the socket died before
|
|
167
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
168
|
+
* across reconnect cycles.
|
|
169
|
+
*/
|
|
170
|
+
clearAllCalls() {
|
|
171
|
+
if (this._callMap.size === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(
|
|
175
|
+
`CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
|
|
176
|
+
);
|
|
177
|
+
for (const call of this._callMap.values()) {
|
|
178
|
+
try {
|
|
179
|
+
call.dispose();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn('CallStateController: Error disposing call during clear:', error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this._callMap.clear();
|
|
185
|
+
this._calls.next([]);
|
|
186
|
+
}
|
|
164
187
|
/**
|
|
165
188
|
* Dispose of the controller and clean up resources
|
|
166
189
|
*/
|
|
@@ -15,6 +15,7 @@ export declare class SessionManager {
|
|
|
15
15
|
private _sessionId;
|
|
16
16
|
private _disposed;
|
|
17
17
|
private _onClientReady?;
|
|
18
|
+
private _onDisconnect?;
|
|
18
19
|
constructor();
|
|
19
20
|
/**
|
|
20
21
|
* Observable stream of connection state changes
|
|
@@ -24,6 +25,11 @@ export declare class SessionManager {
|
|
|
24
25
|
* Set callback to be called when the Telnyx client is ready
|
|
25
26
|
*/
|
|
26
27
|
setOnClientReady(callback: () => void): void;
|
|
28
|
+
/**
|
|
29
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
30
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
31
|
+
*/
|
|
32
|
+
setOnDisconnect(callback: () => void): void;
|
|
27
33
|
/**
|
|
28
34
|
* Current connection state (synchronous access)
|
|
29
35
|
*/
|
|
@@ -45,7 +51,14 @@ export declare class SessionManager {
|
|
|
45
51
|
*/
|
|
46
52
|
connectWithToken(config: TokenConfig): Promise<void>;
|
|
47
53
|
/**
|
|
48
|
-
* Disconnect from the Telnyx platform
|
|
54
|
+
* Disconnect from the Telnyx platform.
|
|
55
|
+
*
|
|
56
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
57
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
58
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
59
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
60
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
61
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
49
62
|
*/
|
|
50
63
|
disconnect(): Promise<void>;
|
|
51
64
|
/**
|
|
@@ -85,6 +85,13 @@ class SessionManager {
|
|
|
85
85
|
setOnClientReady(callback) {
|
|
86
86
|
this._onClientReady = callback;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
90
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
91
|
+
*/
|
|
92
|
+
setOnDisconnect(callback) {
|
|
93
|
+
this._onDisconnect = callback;
|
|
94
|
+
}
|
|
88
95
|
/**
|
|
89
96
|
* Current connection state (synchronous access)
|
|
90
97
|
*/
|
|
@@ -124,13 +131,28 @@ class SessionManager {
|
|
|
124
131
|
await this._connect();
|
|
125
132
|
}
|
|
126
133
|
/**
|
|
127
|
-
* Disconnect from the Telnyx platform
|
|
134
|
+
* Disconnect from the Telnyx platform.
|
|
135
|
+
*
|
|
136
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
137
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
138
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
139
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
140
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
141
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
128
142
|
*/
|
|
129
143
|
async disconnect() {
|
|
130
144
|
if (this._disposed) {
|
|
131
145
|
return;
|
|
132
146
|
}
|
|
133
147
|
this._currentConfig = undefined;
|
|
148
|
+
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
149
|
+
if (this._onDisconnect) {
|
|
150
|
+
try {
|
|
151
|
+
this._onDisconnect();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error in onDisconnect callback:', error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
134
156
|
if (this._telnyxClient) {
|
|
135
157
|
try {
|
|
136
158
|
await this._telnyxClient.disconnect();
|
|
@@ -138,7 +160,6 @@ class SessionManager {
|
|
|
138
160
|
console.error('Error during disconnect:', error);
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
|
-
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
142
163
|
}
|
|
143
164
|
/**
|
|
144
165
|
* Disable push notifications for the current session.
|
package/lib/models/call.d.ts
CHANGED
|
@@ -171,6 +171,18 @@ export declare class Call {
|
|
|
171
171
|
* Toggle mute state
|
|
172
172
|
*/
|
|
173
173
|
toggleMute(): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* Send DTMF tones on this call.
|
|
176
|
+
*
|
|
177
|
+
* Each character in `digits` is sent as a Verto INFO message to the Telnyx
|
|
178
|
+
* platform. Valid characters are `0-9`, `A-D`, `*`, and `#`; any other
|
|
179
|
+
* characters are silently dropped by the underlying SDK.
|
|
180
|
+
*
|
|
181
|
+
* Only valid while the call is `ACTIVE` — will throw otherwise. Safe to call
|
|
182
|
+
* with a single digit (e.g. for IVR dialpad presses) or a whole string
|
|
183
|
+
* (e.g. `"123#"`).
|
|
184
|
+
*/
|
|
185
|
+
dtmf(digits: string): Promise<void>;
|
|
174
186
|
/**
|
|
175
187
|
* Set the call to connecting state (used for push notification calls when answered via CallKit)
|
|
176
188
|
* @internal
|
package/lib/models/call.js
CHANGED
|
@@ -374,6 +374,28 @@ class Call {
|
|
|
374
374
|
await this.mute();
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Send DTMF tones on this call.
|
|
379
|
+
*
|
|
380
|
+
* Each character in `digits` is sent as a Verto INFO message to the Telnyx
|
|
381
|
+
* platform. Valid characters are `0-9`, `A-D`, `*`, and `#`; any other
|
|
382
|
+
* characters are silently dropped by the underlying SDK.
|
|
383
|
+
*
|
|
384
|
+
* Only valid while the call is `ACTIVE` — will throw otherwise. Safe to call
|
|
385
|
+
* with a single digit (e.g. for IVR dialpad presses) or a whole string
|
|
386
|
+
* (e.g. `"123#"`).
|
|
387
|
+
*/
|
|
388
|
+
async dtmf(digits) {
|
|
389
|
+
if (this.currentState !== call_state_1.TelnyxCallState.ACTIVE) {
|
|
390
|
+
throw new Error(`Cannot send DTMF in state: ${this.currentState}`);
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
await this._telnyxCall.dtmf(digits);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('Failed to send DTMF:', error);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
377
399
|
/**
|
|
378
400
|
* Set the call to connecting state (used for push notification calls when answered via CallKit)
|
|
379
401
|
* @internal
|
|
@@ -66,6 +66,11 @@ class TelnyxVoipClient {
|
|
|
66
66
|
);
|
|
67
67
|
this._callStateController.initializeClientListeners();
|
|
68
68
|
});
|
|
69
|
+
// Clear any tracked calls when the session disconnects, so ghosts
|
|
70
|
+
// don't accumulate across background → foreground reconnect cycles.
|
|
71
|
+
this._sessionManager.setOnDisconnect(() => {
|
|
72
|
+
this._callStateController.clearAllCalls();
|
|
73
|
+
});
|
|
69
74
|
if (this._options.debug) {
|
|
70
75
|
console.log('TelnyxVoipClient initialized with options:', this._options);
|
|
71
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"android": "expo run:android",
|
|
40
40
|
"ios": "expo run:ios",
|
|
41
41
|
"dev:local": "npm pkg set dependencies.@telnyx/react-native-voice-sdk=file:../package",
|
|
42
|
-
"dev:published": "npm pkg set \"dependencies.@telnyx/react-native-voice-sdk\"=\">=0.4.
|
|
42
|
+
"dev:published": "npm pkg set \"dependencies.@telnyx/react-native-voice-sdk\"=\">=0.4.2\"",
|
|
43
43
|
"prepublishOnly": "npm run dev:published && npm install --legacy-peer-deps",
|
|
44
44
|
"postpublish": "npm run dev:local && npm install --legacy-peer-deps"
|
|
45
45
|
},
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"homepage": "https://github.com/team-telnyx/react-native-voice-commons",
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
62
|
-
"url": "
|
|
62
|
+
"url": "https://github.com/team-telnyx/react-native-voice-commons.git"
|
|
63
63
|
},
|
|
64
64
|
"bugs": {
|
|
65
65
|
"url": "https://github.com/team-telnyx/react-native-voice-commons/issues"
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
89
|
-
"@telnyx/react-native-voice-sdk": ">=0.4.
|
|
89
|
+
"@telnyx/react-native-voice-sdk": ">=0.4.2",
|
|
90
90
|
"eventemitter3": "^5.0.1",
|
|
91
91
|
"expo": "~53.0.22",
|
|
92
92
|
"react-native-url-polyfill": "^3.0.0",
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
"node": ">=16"
|
|
113
113
|
},
|
|
114
114
|
"publishConfig": {
|
|
115
|
-
"access": "public"
|
|
115
|
+
"access": "public",
|
|
116
|
+
"provenance": true
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -190,6 +190,32 @@ export class CallStateController {
|
|
|
190
190
|
this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
195
|
+
* calls left in non-terminal states (because the socket died before
|
|
196
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
197
|
+
* across reconnect cycles.
|
|
198
|
+
*/
|
|
199
|
+
clearAllCalls(): void {
|
|
200
|
+
if (this._callMap.size === 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(
|
|
205
|
+
`CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
for (const call of this._callMap.values()) {
|
|
209
|
+
try {
|
|
210
|
+
call.dispose();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn('CallStateController: Error disposing call during clear:', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this._callMap.clear();
|
|
216
|
+
this._calls.next([]);
|
|
217
|
+
}
|
|
218
|
+
|
|
193
219
|
/**
|
|
194
220
|
* Dispose of the controller and clean up resources
|
|
195
221
|
*/
|
|
@@ -26,6 +26,7 @@ export class SessionManager {
|
|
|
26
26
|
private _sessionId: string;
|
|
27
27
|
private _disposed = false;
|
|
28
28
|
private _onClientReady?: () => void;
|
|
29
|
+
private _onDisconnect?: () => void;
|
|
29
30
|
|
|
30
31
|
constructor() {
|
|
31
32
|
this._sessionId = this._generateSessionId();
|
|
@@ -45,6 +46,14 @@ export class SessionManager {
|
|
|
45
46
|
this._onClientReady = callback;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
51
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
52
|
+
*/
|
|
53
|
+
setOnDisconnect(callback: () => void): void {
|
|
54
|
+
this._onDisconnect = callback;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
/**
|
|
49
58
|
* Current connection state (synchronous access)
|
|
50
59
|
*/
|
|
@@ -91,7 +100,14 @@ export class SessionManager {
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
|
-
* Disconnect from the Telnyx platform
|
|
103
|
+
* Disconnect from the Telnyx platform.
|
|
104
|
+
*
|
|
105
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
106
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
107
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
108
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
109
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
110
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
95
111
|
*/
|
|
96
112
|
async disconnect(): Promise<void> {
|
|
97
113
|
if (this._disposed) {
|
|
@@ -99,6 +115,15 @@ export class SessionManager {
|
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
this._currentConfig = undefined;
|
|
118
|
+
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
119
|
+
|
|
120
|
+
if (this._onDisconnect) {
|
|
121
|
+
try {
|
|
122
|
+
this._onDisconnect();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error in onDisconnect callback:', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
if (this._telnyxClient) {
|
|
104
129
|
try {
|
|
@@ -107,8 +132,6 @@ export class SessionManager {
|
|
|
107
132
|
console.error('Error during disconnect:', error);
|
|
108
133
|
}
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
/**
|
package/src/models/call.ts
CHANGED
|
@@ -353,6 +353,30 @@ export class Call {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Send DTMF tones on this call.
|
|
358
|
+
*
|
|
359
|
+
* Each character in `digits` is sent as a Verto INFO message to the Telnyx
|
|
360
|
+
* platform. Valid characters are `0-9`, `A-D`, `*`, and `#`; any other
|
|
361
|
+
* characters are silently dropped by the underlying SDK.
|
|
362
|
+
*
|
|
363
|
+
* Only valid while the call is `ACTIVE` — will throw otherwise. Safe to call
|
|
364
|
+
* with a single digit (e.g. for IVR dialpad presses) or a whole string
|
|
365
|
+
* (e.g. `"123#"`).
|
|
366
|
+
*/
|
|
367
|
+
async dtmf(digits: string): Promise<void> {
|
|
368
|
+
if (this.currentState !== TelnyxCallState.ACTIVE) {
|
|
369
|
+
throw new Error(`Cannot send DTMF in state: ${this.currentState}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
await this._telnyxCall.dtmf(digits);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('Failed to send DTMF:', error);
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
356
380
|
/**
|
|
357
381
|
* Set the call to connecting state (used for push notification calls when answered via CallKit)
|
|
358
382
|
* @internal
|
|
@@ -82,6 +82,12 @@ export class TelnyxVoipClient {
|
|
|
82
82
|
this._callStateController.initializeClientListeners();
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
// Clear any tracked calls when the session disconnects, so ghosts
|
|
86
|
+
// don't accumulate across background → foreground reconnect cycles.
|
|
87
|
+
this._sessionManager.setOnDisconnect(() => {
|
|
88
|
+
this._callStateController.clearAllCalls();
|
|
89
|
+
});
|
|
90
|
+
|
|
85
91
|
if (this._options.debug) {
|
|
86
92
|
console.log('TelnyxVoipClient initialized with options:', this._options);
|
|
87
93
|
}
|