@telnyx/react-voice-commons-sdk 0.3.1 → 0.4.1
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 +25 -0
- package/README.md +108 -11
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +5 -4
- package/lib/internal/session/session-manager.js +41 -3
- package/lib/models/call.d.ts +12 -0
- package/lib/models/call.js +22 -0
- package/package.json +3 -3
- package/src/internal/session/session-manager.ts +45 -3
- package/src/models/call.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# CHANGELOG.md
|
|
2
2
|
|
|
3
|
+
## [0.4.1] (2026-04-27)
|
|
4
|
+
|
|
5
|
+
### Bug Fixing
|
|
6
|
+
|
|
7
|
+
- **Recover from stale TelnyxRTC client on incoming VoIP push.** `SessionManager.handlePushNotification` now calls `client.isFresh(30_000)` before reusing an existing client. If the socket has been silent past the threshold (typical after an iOS app freeze), the client is disposed, state is flipped to `DISCONNECTED`, and the push falls through to the full `_connect()` path so login runs with the new push's `voice_sdk_id`. This resolves the case where an answered call would hang on "connecting" until CallKit timed out, because `processVoIPNotification()` was being called on a socket that had silently died.
|
|
8
|
+
- Widened the reconnect-trigger state check in `handlePushNotification` to include `ERROR`, not only `DISCONNECTED`, so a push that arrives after a failed session re-establishes the connection instead of being dropped.
|
|
9
|
+
|
|
10
|
+
### Dependencies
|
|
11
|
+
|
|
12
|
+
- Now requires `@telnyx/react-native-voice-sdk >= 0.4.3`, which adds the `isFresh()` / `connectionIdleMs` API and auto-reconnects on unexpected socket errors. See the [voice-sdk 0.4.3 changelog](../package/CHANGELOG.md#043-2026-04-27) for details.
|
|
13
|
+
|
|
14
|
+
## [0.4.0] (2026-04-19)
|
|
15
|
+
|
|
16
|
+
### Enhancement
|
|
17
|
+
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
20
|
+
### Bug Fixing
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
|
|
26
|
+
- 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.
|
|
27
|
+
|
|
3
28
|
## [0.3.1] (2026-04-16)
|
|
4
29
|
|
|
5
30
|
### Bug Fixing
|
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) {
|
|
@@ -202,6 +202,39 @@ class SessionManager {
|
|
|
202
202
|
);
|
|
203
203
|
// Store the push notification payload for when the client is created
|
|
204
204
|
this._pendingPushPayload = payload;
|
|
205
|
+
// If we have a TelnyxRTC instance but its socket isn't fresh — either not
|
|
206
|
+
// connected at all, or connected but with no traffic for longer than the
|
|
207
|
+
// threshold — dispose it so we go through the full _connect() path below
|
|
208
|
+
// instead of calling processVoIPNotification on a stale client.
|
|
209
|
+
//
|
|
210
|
+
// This handles two cases iOS gives us with the same code path:
|
|
211
|
+
// 1. Terminated-then-cold-launched: a client exists (constructed at
|
|
212
|
+
// app boot) but its connection was never opened (idleMs=Infinity).
|
|
213
|
+
// 2. Suspended-then-thawed: a client exists with `connected=true`,
|
|
214
|
+
// but the kernel killed the TLS session during freeze without
|
|
215
|
+
// notifying the JS WebSocket wrapper, so no error event fired.
|
|
216
|
+
//
|
|
217
|
+
// 30s threshold matches the server's keep-alive ping cadence (~20s),
|
|
218
|
+
// so a live session always remains fresh.
|
|
219
|
+
const STALE_THRESHOLD_MS = 30000;
|
|
220
|
+
if (this._telnyxClient) {
|
|
221
|
+
const client = this._telnyxClient;
|
|
222
|
+
const isFresh =
|
|
223
|
+
typeof client.isFresh === 'function'
|
|
224
|
+
? client.isFresh(STALE_THRESHOLD_MS)
|
|
225
|
+
: !!client.connected;
|
|
226
|
+
if (!isFresh) {
|
|
227
|
+
try {
|
|
228
|
+
await this._telnyxClient.disconnect();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn('SessionManager: disconnect of stale client threw:', err);
|
|
231
|
+
}
|
|
232
|
+
this._telnyxClient = undefined;
|
|
233
|
+
if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
|
|
234
|
+
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
205
238
|
// If we don't have a config yet but we're processing a push notification,
|
|
206
239
|
// attempt to load stored config first (for terminated app startup)
|
|
207
240
|
if (!this._currentConfig && !this._telnyxClient) {
|
|
@@ -269,11 +302,16 @@ class SessionManager {
|
|
|
269
302
|
console.log(
|
|
270
303
|
'SessionManager: RELEASE DEBUG - No client available, checking if we can trigger immediate connection'
|
|
271
304
|
);
|
|
272
|
-
// If we have config (either existing or newly loaded from storage) and
|
|
273
|
-
//
|
|
305
|
+
// If we have config (either existing or newly loaded from storage) and
|
|
306
|
+
// are not currently connected/connecting, trigger immediate connection.
|
|
307
|
+
// We accept DISCONNECTED and ERROR (a socket failure bumps state to
|
|
308
|
+
// ERROR) so a push after a failed session still re-establishes the
|
|
309
|
+
// connection. The _connect() method will process the pending push
|
|
310
|
+
// payload BEFORE calling connect().
|
|
274
311
|
if (
|
|
275
312
|
this._currentConfig &&
|
|
276
|
-
this.currentState === connection_state_1.TelnyxConnectionState.DISCONNECTED
|
|
313
|
+
(this.currentState === connection_state_1.TelnyxConnectionState.DISCONNECTED ||
|
|
314
|
+
this.currentState === connection_state_1.TelnyxConnectionState.ERROR)
|
|
277
315
|
) {
|
|
278
316
|
console.log(
|
|
279
317
|
'SessionManager: RELEASE DEBUG - Triggering immediate connection for push notification with config type:',
|
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
|
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.1",
|
|
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
|
},
|
|
@@ -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",
|
|
@@ -178,6 +178,40 @@ export class SessionManager {
|
|
|
178
178
|
// Store the push notification payload for when the client is created
|
|
179
179
|
(this as any)._pendingPushPayload = payload;
|
|
180
180
|
|
|
181
|
+
// If we have a TelnyxRTC instance but its socket isn't fresh — either not
|
|
182
|
+
// connected at all, or connected but with no traffic for longer than the
|
|
183
|
+
// threshold — dispose it so we go through the full _connect() path below
|
|
184
|
+
// instead of calling processVoIPNotification on a stale client.
|
|
185
|
+
//
|
|
186
|
+
// This handles two cases iOS gives us with the same code path:
|
|
187
|
+
// 1. Terminated-then-cold-launched: a client exists (constructed at
|
|
188
|
+
// app boot) but its connection was never opened (idleMs=Infinity).
|
|
189
|
+
// 2. Suspended-then-thawed: a client exists with `connected=true`,
|
|
190
|
+
// but the kernel killed the TLS session during freeze without
|
|
191
|
+
// notifying the JS WebSocket wrapper, so no error event fired.
|
|
192
|
+
//
|
|
193
|
+
// 30s threshold matches the server's keep-alive ping cadence (~20s),
|
|
194
|
+
// so a live session always remains fresh.
|
|
195
|
+
const STALE_THRESHOLD_MS = 30000;
|
|
196
|
+
if (this._telnyxClient) {
|
|
197
|
+
const client = this._telnyxClient as any;
|
|
198
|
+
const isFresh =
|
|
199
|
+
typeof client.isFresh === 'function'
|
|
200
|
+
? client.isFresh(STALE_THRESHOLD_MS)
|
|
201
|
+
: !!client.connected;
|
|
202
|
+
if (!isFresh) {
|
|
203
|
+
try {
|
|
204
|
+
await this._telnyxClient.disconnect();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn('SessionManager: disconnect of stale client threw:', err);
|
|
207
|
+
}
|
|
208
|
+
this._telnyxClient = undefined;
|
|
209
|
+
if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
|
|
210
|
+
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
181
215
|
// If we don't have a config yet but we're processing a push notification,
|
|
182
216
|
// attempt to load stored config first (for terminated app startup)
|
|
183
217
|
if (!this._currentConfig && !this._telnyxClient) {
|
|
@@ -255,9 +289,17 @@ export class SessionManager {
|
|
|
255
289
|
'SessionManager: RELEASE DEBUG - No client available, checking if we can trigger immediate connection'
|
|
256
290
|
);
|
|
257
291
|
|
|
258
|
-
// If we have config (either existing or newly loaded from storage) and
|
|
259
|
-
//
|
|
260
|
-
|
|
292
|
+
// If we have config (either existing or newly loaded from storage) and
|
|
293
|
+
// are not currently connected/connecting, trigger immediate connection.
|
|
294
|
+
// We accept DISCONNECTED and ERROR (a socket failure bumps state to
|
|
295
|
+
// ERROR) so a push after a failed session still re-establishes the
|
|
296
|
+
// connection. The _connect() method will process the pending push
|
|
297
|
+
// payload BEFORE calling connect().
|
|
298
|
+
if (
|
|
299
|
+
this._currentConfig &&
|
|
300
|
+
(this.currentState === TelnyxConnectionState.DISCONNECTED ||
|
|
301
|
+
this.currentState === TelnyxConnectionState.ERROR)
|
|
302
|
+
) {
|
|
261
303
|
console.log(
|
|
262
304
|
'SessionManager: RELEASE DEBUG - Triggering immediate connection for push notification with config type:',
|
|
263
305
|
(this._currentConfig as any).type || 'credential'
|
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
|