@telnyx/react-voice-commons-sdk 1.1.1 → 1.2.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 +45 -1
- package/TelnyxVoiceCommons.podspec +3 -3
- package/ios/CallKitBridge.swift +126 -44
- package/lib/callkit/callkit-coordinator.d.ts +12 -0
- package/lib/callkit/callkit-coordinator.js +32 -4
- package/lib/index.d.ts +4 -1
- package/lib/index.js +9 -0
- package/lib/internal/calls/call-state-controller.d.ts +1 -0
- package/lib/internal/calls/call-state-controller.js +35 -1
- package/lib/internal/ice-servers-storage.d.ts +9 -0
- package/lib/internal/ice-servers-storage.js +37 -0
- package/lib/internal/sdk-identity.d.ts +4 -0
- package/lib/internal/sdk-identity.js +60 -0
- package/lib/internal/session/session-manager.d.ts +7 -2
- package/lib/internal/session/session-manager.js +35 -4
- package/lib/internal/signaling-environment.d.ts +13 -0
- package/lib/internal/signaling-environment.js +18 -0
- package/lib/models/call.d.ts +23 -2
- package/lib/models/call.js +41 -3
- package/lib/models/config.d.ts +28 -0
- package/lib/telnyx-voip-client.d.ts +4 -2
- package/lib/telnyx-voip-client.js +60 -2
- package/package.json +3 -4
- package/src/callkit/callkit-coordinator.ts +49 -6
- package/src/index.ts +6 -1
- package/src/internal/calls/call-state-controller.ts +41 -2
- package/src/internal/ice-servers-storage.ts +37 -0
- package/src/internal/sdk-identity.ts +6 -0
- package/src/internal/session/session-manager.ts +38 -4
- package/src/internal/signaling-environment.ts +20 -0
- package/src/models/call.ts +48 -3
- package/src/models/config.ts +30 -0
- package/src/telnyx-voip-client.ts +60 -3
- package/src/types/telnyx-sdk.d.ts +67 -2
|
@@ -56,12 +56,15 @@ exports.SessionManager = void 0;
|
|
|
56
56
|
const rxjs_1 = require('rxjs');
|
|
57
57
|
const operators_1 = require('rxjs/operators');
|
|
58
58
|
const TelnyxSDK = __importStar(require('@telnyx/react-native-voice-sdk'));
|
|
59
|
-
const
|
|
59
|
+
const sdk_identity_1 = require('../sdk-identity');
|
|
60
|
+
const ice_servers_storage_1 = require('../ice-servers-storage');
|
|
61
|
+
const signaling_environment_1 = require('../signaling-environment');
|
|
60
62
|
const connection_state_1 = require('../../models/connection-state');
|
|
61
63
|
const config_1 = require('../../models/config');
|
|
62
64
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
63
65
|
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
64
66
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
67
|
+
const QUALITY_METRICS_STORAGE_KEY = '@enable_quality_metrics';
|
|
65
68
|
/**
|
|
66
69
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
67
70
|
*
|
|
@@ -69,7 +72,7 @@ const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications
|
|
|
69
72
|
* and automatic reconnection logic.
|
|
70
73
|
*/
|
|
71
74
|
class SessionManager {
|
|
72
|
-
constructor() {
|
|
75
|
+
constructor(options = {}) {
|
|
73
76
|
this._connectionState = new rxjs_1.BehaviorSubject(
|
|
74
77
|
connection_state_1.TelnyxConnectionState.DISCONNECTED
|
|
75
78
|
);
|
|
@@ -77,6 +80,7 @@ class SessionManager {
|
|
|
77
80
|
this._disposing = false;
|
|
78
81
|
this._connectionGeneration = 0;
|
|
79
82
|
this._sessionId = this._generateSessionId();
|
|
83
|
+
this._defaultEnvironment = options.defaultEnvironment ?? 'production';
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Observable stream of connection state changes
|
|
@@ -254,9 +258,22 @@ class SessionManager {
|
|
|
254
258
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
255
259
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
256
260
|
);
|
|
261
|
+
const storedEnvironment = await AsyncStorage.getItem(
|
|
262
|
+
signaling_environment_1.SIGNALING_ENVIRONMENT_STORAGE_KEY
|
|
263
|
+
);
|
|
264
|
+
const storedQualityMetrics = await AsyncStorage.getItem(QUALITY_METRICS_STORAGE_KEY);
|
|
265
|
+
const storedIceServers = await AsyncStorage.getItem(
|
|
266
|
+
ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY
|
|
267
|
+
);
|
|
268
|
+
const iceServers = (0, ice_servers_storage_1.parseStoredIceServers)(storedIceServers);
|
|
257
269
|
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
258
270
|
const pushWhenActive = storedPushWhenActive === 'true';
|
|
259
271
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
272
|
+
const enableQualityMetrics = storedQualityMetrics === 'true';
|
|
273
|
+
const environment = (0, signaling_environment_1.resolveStoredEnvironment)(
|
|
274
|
+
storedEnvironment,
|
|
275
|
+
this._defaultEnvironment
|
|
276
|
+
);
|
|
260
277
|
if (this._isTeardownActive()) {
|
|
261
278
|
return;
|
|
262
279
|
}
|
|
@@ -269,6 +286,9 @@ class SessionManager {
|
|
|
269
286
|
useTrickleIce,
|
|
270
287
|
pushWhenActive,
|
|
271
288
|
enableMissedCallNotifications,
|
|
289
|
+
enableQualityMetrics,
|
|
290
|
+
environment,
|
|
291
|
+
iceServers,
|
|
272
292
|
});
|
|
273
293
|
}
|
|
274
294
|
// Check if we have token-based authentication data
|
|
@@ -280,6 +300,9 @@ class SessionManager {
|
|
|
280
300
|
useTrickleIce,
|
|
281
301
|
pushWhenActive,
|
|
282
302
|
enableMissedCallNotifications,
|
|
303
|
+
enableQualityMetrics,
|
|
304
|
+
environment,
|
|
305
|
+
iceServers,
|
|
283
306
|
});
|
|
284
307
|
}
|
|
285
308
|
if (this._currentConfig) {
|
|
@@ -463,11 +486,15 @@ class SessionManager {
|
|
|
463
486
|
pushWhenActive: config.pushWhenActive,
|
|
464
487
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
465
488
|
useTrickleIce: config.useTrickleIce,
|
|
489
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
466
490
|
enableCallReports: config.enableCallReports,
|
|
491
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
467
492
|
callReportInterval: config.callReportInterval,
|
|
468
493
|
callReportLogLevel: config.callReportLogLevel,
|
|
469
494
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
470
|
-
|
|
495
|
+
iceServers: config.iceServers,
|
|
496
|
+
sdkName: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
497
|
+
sdkVersion: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
471
498
|
};
|
|
472
499
|
console.log(
|
|
473
500
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
@@ -484,11 +511,15 @@ class SessionManager {
|
|
|
484
511
|
pushWhenActive: config.pushWhenActive,
|
|
485
512
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
486
513
|
useTrickleIce: config.useTrickleIce,
|
|
514
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
487
515
|
enableCallReports: config.enableCallReports,
|
|
516
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
488
517
|
callReportInterval: config.callReportInterval,
|
|
489
518
|
callReportLogLevel: config.callReportLogLevel,
|
|
490
519
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
491
|
-
|
|
520
|
+
iceServers: config.iceServers,
|
|
521
|
+
sdkName: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
522
|
+
sdkVersion: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
492
523
|
};
|
|
493
524
|
console.log(
|
|
494
525
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TelnyxEnvironment } from '../models/config';
|
|
2
|
+
export declare const SIGNALING_ENVIRONMENT_STORAGE_KEY = '@telnyx_signaling_environment';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the signaling environment for a stored-config restore.
|
|
5
|
+
*
|
|
6
|
+
* An explicit stored selection always wins. A missing or invalid stored value
|
|
7
|
+
* (e.g. credentials persisted by an older Commons version) falls back to the
|
|
8
|
+
* client's configured default, and only then to production.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveStoredEnvironment(
|
|
11
|
+
storedEnvironment: string | null | undefined,
|
|
12
|
+
defaultEnvironment?: TelnyxEnvironment
|
|
13
|
+
): TelnyxEnvironment;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.SIGNALING_ENVIRONMENT_STORAGE_KEY = void 0;
|
|
4
|
+
exports.resolveStoredEnvironment = resolveStoredEnvironment;
|
|
5
|
+
exports.SIGNALING_ENVIRONMENT_STORAGE_KEY = '@telnyx_signaling_environment';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the signaling environment for a stored-config restore.
|
|
8
|
+
*
|
|
9
|
+
* An explicit stored selection always wins. A missing or invalid stored value
|
|
10
|
+
* (e.g. credentials persisted by an older Commons version) falls back to the
|
|
11
|
+
* client's configured default, and only then to production.
|
|
12
|
+
*/
|
|
13
|
+
function resolveStoredEnvironment(storedEnvironment, defaultEnvironment) {
|
|
14
|
+
if (storedEnvironment === 'development' || storedEnvironment === 'production') {
|
|
15
|
+
return storedEnvironment;
|
|
16
|
+
}
|
|
17
|
+
return defaultEnvironment ?? 'production';
|
|
18
|
+
}
|
package/lib/models/call.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { TelnyxCallState } from './call-state';
|
|
3
3
|
import { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
4
|
+
import type { CallQualityMetrics } from '@telnyx/react-native-voice-sdk';
|
|
4
5
|
/**
|
|
5
6
|
* Represents a call with reactive state streams.
|
|
6
7
|
*
|
|
@@ -19,6 +20,8 @@ export declare class Call {
|
|
|
19
20
|
private readonly _isMuted;
|
|
20
21
|
private readonly _isHeld;
|
|
21
22
|
private readonly _duration;
|
|
23
|
+
private readonly _qualityMetrics;
|
|
24
|
+
private readonly _onQualityMetrics;
|
|
22
25
|
private _durationTimer?;
|
|
23
26
|
private _startTime?;
|
|
24
27
|
constructor(
|
|
@@ -75,7 +78,7 @@ export declare class Call {
|
|
|
75
78
|
/**
|
|
76
79
|
* Custom headers received from the WebRTC INVITE message.
|
|
77
80
|
* These headers are passed during call initiation and can contain application-specific information.
|
|
78
|
-
* Format
|
|
81
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
79
82
|
*/
|
|
80
83
|
get inviteCustomHeaders():
|
|
81
84
|
| {
|
|
@@ -86,7 +89,7 @@ export declare class Call {
|
|
|
86
89
|
/**
|
|
87
90
|
* Custom headers received from the WebRTC ANSWER message.
|
|
88
91
|
* These headers are passed during call acceptance and can contain application-specific information.
|
|
89
|
-
* Format
|
|
92
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
90
93
|
*/
|
|
91
94
|
get answerCustomHeaders():
|
|
92
95
|
| {
|
|
@@ -99,6 +102,13 @@ export declare class Call {
|
|
|
99
102
|
* @internal
|
|
100
103
|
*/
|
|
101
104
|
get telnyxCall(): TelnyxCall;
|
|
105
|
+
/**
|
|
106
|
+
* Inject an ICE failure into the underlying SDK recovery flow for manual
|
|
107
|
+
* testing. The underlying SDK accepts this only when debug mode is enabled.
|
|
108
|
+
*
|
|
109
|
+
* @returns true when a recovery episode was started.
|
|
110
|
+
*/
|
|
111
|
+
simulateIceFailureForTesting(): boolean;
|
|
102
112
|
/**
|
|
103
113
|
* Observable stream of call state changes
|
|
104
114
|
*/
|
|
@@ -115,6 +125,17 @@ export declare class Call {
|
|
|
115
125
|
* Observable stream of call duration changes (in seconds)
|
|
116
126
|
*/
|
|
117
127
|
get duration$(): Observable<number>;
|
|
128
|
+
/**
|
|
129
|
+
* Observable stream of call quality metrics updates.
|
|
130
|
+
* Emits `null` initially, then a {@link CallQualityMetrics} object
|
|
131
|
+
* each time the underlying SDK collects a new sample.
|
|
132
|
+
*/
|
|
133
|
+
get qualityMetrics$(): Observable<CallQualityMetrics | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Current call quality metrics (synchronous access).
|
|
136
|
+
* Returns `null` until the first metrics sample is collected.
|
|
137
|
+
*/
|
|
138
|
+
get currentQualityMetrics(): CallQualityMetrics | null;
|
|
118
139
|
/**
|
|
119
140
|
* Observable that emits true when the call can be answered
|
|
120
141
|
*/
|
package/lib/models/call.js
CHANGED
|
@@ -84,6 +84,10 @@ class Call {
|
|
|
84
84
|
this._isMuted = new rxjs_1.BehaviorSubject(false);
|
|
85
85
|
this._isHeld = new rxjs_1.BehaviorSubject(false);
|
|
86
86
|
this._duration = new rxjs_1.BehaviorSubject(0);
|
|
87
|
+
this._qualityMetrics = new rxjs_1.BehaviorSubject(null);
|
|
88
|
+
this._onQualityMetrics = (metrics) => {
|
|
89
|
+
this._qualityMetrics.next(metrics);
|
|
90
|
+
};
|
|
87
91
|
// Set initial state based on whether this is a reattached call
|
|
88
92
|
if (isReattached) {
|
|
89
93
|
console.log('Call: Setting initial state to ACTIVE for reattached call');
|
|
@@ -156,7 +160,7 @@ class Call {
|
|
|
156
160
|
/**
|
|
157
161
|
* Custom headers received from the WebRTC INVITE message.
|
|
158
162
|
* These headers are passed during call initiation and can contain application-specific information.
|
|
159
|
-
* Format
|
|
163
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
160
164
|
*/
|
|
161
165
|
get inviteCustomHeaders() {
|
|
162
166
|
return this._telnyxCall.inviteCustomHeaders;
|
|
@@ -164,7 +168,7 @@ class Call {
|
|
|
164
168
|
/**
|
|
165
169
|
* Custom headers received from the WebRTC ANSWER message.
|
|
166
170
|
* These headers are passed during call acceptance and can contain application-specific information.
|
|
167
|
-
* Format
|
|
171
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
168
172
|
*/
|
|
169
173
|
get answerCustomHeaders() {
|
|
170
174
|
return this._telnyxCall.answerCustomHeaders;
|
|
@@ -176,6 +180,16 @@ class Call {
|
|
|
176
180
|
get telnyxCall() {
|
|
177
181
|
return this._telnyxCall;
|
|
178
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Inject an ICE failure into the underlying SDK recovery flow for manual
|
|
185
|
+
* testing. The underlying SDK accepts this only when debug mode is enabled.
|
|
186
|
+
*
|
|
187
|
+
* @returns true when a recovery episode was started.
|
|
188
|
+
*/
|
|
189
|
+
simulateIceFailureForTesting() {
|
|
190
|
+
const testableCall = this._telnyxCall;
|
|
191
|
+
return testableCall.simulateIceFailureForTesting?.() ?? false;
|
|
192
|
+
}
|
|
179
193
|
/**
|
|
180
194
|
* Observable stream of call state changes
|
|
181
195
|
*/
|
|
@@ -200,6 +214,21 @@ class Call {
|
|
|
200
214
|
get duration$() {
|
|
201
215
|
return this._duration.asObservable().pipe((0, operators_1.distinctUntilChanged)());
|
|
202
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Observable stream of call quality metrics updates.
|
|
219
|
+
* Emits `null` initially, then a {@link CallQualityMetrics} object
|
|
220
|
+
* each time the underlying SDK collects a new sample.
|
|
221
|
+
*/
|
|
222
|
+
get qualityMetrics$() {
|
|
223
|
+
return this._qualityMetrics.asObservable().pipe((0, operators_1.distinctUntilChanged)());
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Current call quality metrics (synchronous access).
|
|
227
|
+
* Returns `null` until the first metrics sample is collected.
|
|
228
|
+
*/
|
|
229
|
+
get currentQualityMetrics() {
|
|
230
|
+
return this._qualityMetrics.value;
|
|
231
|
+
}
|
|
203
232
|
/**
|
|
204
233
|
* Observable that emits true when the call can be answered
|
|
205
234
|
*/
|
|
@@ -252,7 +281,10 @@ class Call {
|
|
|
252
281
|
);
|
|
253
282
|
if (callKitCoordinator.isAvailable()) {
|
|
254
283
|
console.log('Call: Using CallKit coordinator to answer call (iOS)');
|
|
255
|
-
await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
284
|
+
const answered = await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
285
|
+
if (!answered) {
|
|
286
|
+
throw new Error('CallKit failed to answer call');
|
|
287
|
+
}
|
|
256
288
|
return;
|
|
257
289
|
}
|
|
258
290
|
}
|
|
@@ -433,10 +465,12 @@ class Call {
|
|
|
433
465
|
*/
|
|
434
466
|
dispose() {
|
|
435
467
|
this._stopDurationTimer();
|
|
468
|
+
this._telnyxCall.off?.('telnyx.call.qualityMetrics', this._onQualityMetrics);
|
|
436
469
|
this._callState.complete();
|
|
437
470
|
this._isMuted.complete();
|
|
438
471
|
this._isHeld.complete();
|
|
439
472
|
this._duration.complete();
|
|
473
|
+
this._qualityMetrics.complete();
|
|
440
474
|
}
|
|
441
475
|
/**
|
|
442
476
|
* Set up listeners for the underlying Telnyx call
|
|
@@ -531,6 +565,10 @@ class Call {
|
|
|
531
565
|
}
|
|
532
566
|
}
|
|
533
567
|
});
|
|
568
|
+
// Forward quality metrics from the underlying SDK
|
|
569
|
+
// Subscribe to the event rather than the single onQualityMetrics slot, so
|
|
570
|
+
// an app can still set that callback on the underlying call.
|
|
571
|
+
this._telnyxCall.on('telnyx.call.qualityMetrics', this._onQualityMetrics);
|
|
534
572
|
}
|
|
535
573
|
/**
|
|
536
574
|
* Map Telnyx SDK call states to our simplified call states
|
package/lib/models/config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
1
2
|
/**
|
|
2
3
|
* Configuration for credential-based authentication
|
|
3
4
|
*/
|
|
5
|
+
export type TelnyxEnvironment = 'production' | 'development';
|
|
4
6
|
export interface CredentialConfig {
|
|
5
7
|
type: 'credential';
|
|
6
8
|
sipUser: string;
|
|
@@ -15,14 +17,27 @@ export interface CredentialConfig {
|
|
|
15
17
|
incomingCallRingtone?: string;
|
|
16
18
|
/** Enable Trickle ICE. Default: false */
|
|
17
19
|
useTrickleIce?: boolean;
|
|
20
|
+
/** Signaling environment. Default: production */
|
|
21
|
+
environment?: TelnyxEnvironment;
|
|
18
22
|
/** Enable automatic call quality reporting. Default: true */
|
|
19
23
|
enableCallReports?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Deliver real-time call quality metrics on `Call.qualityMetrics$` while a
|
|
26
|
+
* call is active. Also enabled by `debug`. Default: false
|
|
27
|
+
*/
|
|
28
|
+
enableQualityMetrics?: boolean;
|
|
20
29
|
/** Stats collection interval in seconds. Default: 5 */
|
|
21
30
|
callReportInterval?: number;
|
|
22
31
|
/** Minimum log level for call reports: 'debug' | 'info' | 'warn' | 'error'. Default: 'debug' */
|
|
23
32
|
callReportLogLevel?: string;
|
|
24
33
|
/** Max log entries per call. Default: 1000 */
|
|
25
34
|
callReportMaxLogEntries?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Custom ICE servers (STUN/TURN) for WebRTC peer connections.
|
|
37
|
+
* When provided, overrides the Telnyx production defaults.
|
|
38
|
+
* When omitted, the SDK uses Telnyx defaults (STUN → TURN UDP → TURN TCP → TURNS 443).
|
|
39
|
+
*/
|
|
40
|
+
iceServers?: TelnyxIceServer[];
|
|
26
41
|
}
|
|
27
42
|
/**
|
|
28
43
|
* Configuration for token-based authentication
|
|
@@ -40,14 +55,27 @@ export interface TokenConfig {
|
|
|
40
55
|
incomingCallRingtone?: string;
|
|
41
56
|
/** Enable Trickle ICE. Default: false */
|
|
42
57
|
useTrickleIce?: boolean;
|
|
58
|
+
/** Signaling environment. Default: production */
|
|
59
|
+
environment?: TelnyxEnvironment;
|
|
43
60
|
/** Enable automatic call quality reporting. Default: true */
|
|
44
61
|
enableCallReports?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Deliver real-time call quality metrics on `Call.qualityMetrics$` while a
|
|
64
|
+
* call is active. Also enabled by `debug`. Default: false
|
|
65
|
+
*/
|
|
66
|
+
enableQualityMetrics?: boolean;
|
|
45
67
|
/** Stats collection interval in seconds. Default: 5 */
|
|
46
68
|
callReportInterval?: number;
|
|
47
69
|
/** Minimum log level for call reports: 'debug' | 'info' | 'warn' | 'error'. Default: 'debug' */
|
|
48
70
|
callReportLogLevel?: string;
|
|
49
71
|
/** Max log entries per call. Default: 1000 */
|
|
50
72
|
callReportMaxLogEntries?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Custom ICE servers (STUN/TURN) for WebRTC peer connections.
|
|
75
|
+
* When provided, overrides the Telnyx production defaults.
|
|
76
|
+
* When omitted, the SDK uses Telnyx defaults (STUN → TURN UDP → TURN TCP → TURNS 443).
|
|
77
|
+
*/
|
|
78
|
+
iceServers?: TelnyxIceServer[];
|
|
51
79
|
}
|
|
52
80
|
/**
|
|
53
81
|
* Union type for all supported authentication configurations
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { TelnyxConnectionState } from './models/connection-state';
|
|
3
3
|
import { Call } from './models/call';
|
|
4
|
-
import { CredentialConfig, TokenConfig } from './models/config';
|
|
4
|
+
import { CredentialConfig, TelnyxEnvironment, TokenConfig } from './models/config';
|
|
5
5
|
import { type CustomHeaders } from './internal/calls/call-state-controller';
|
|
6
6
|
/**
|
|
7
7
|
* Configuration options for TelnyxVoipClient
|
|
@@ -13,6 +13,8 @@ export interface TelnyxVoipClientOptions {
|
|
|
13
13
|
debug?: boolean;
|
|
14
14
|
/** Enable Trickle ICE for calls created by this client */
|
|
15
15
|
useTrickleIce?: boolean;
|
|
16
|
+
/** Signaling environment used for connections - default: production */
|
|
17
|
+
environment?: TelnyxEnvironment;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* The main public interface for the react-voice-commons module.
|
|
@@ -162,7 +164,7 @@ export declare class TelnyxVoipClient {
|
|
|
162
164
|
* This method is used for auto-reconnection scenarios where the app
|
|
163
165
|
* comes back to the foreground and needs to restore the connection.
|
|
164
166
|
*
|
|
165
|
-
* @returns
|
|
167
|
+
* @returns Whether reconnection was successful.
|
|
166
168
|
*/
|
|
167
169
|
loginFromStoredConfig(): Promise<boolean>;
|
|
168
170
|
/**
|
|
@@ -61,11 +61,14 @@ const connection_state_1 = require('./models/connection-state');
|
|
|
61
61
|
const call_state_1 = require('./models/call-state');
|
|
62
62
|
const config_1 = require('./models/config');
|
|
63
63
|
const session_manager_1 = require('./internal/session/session-manager');
|
|
64
|
+
const signaling_environment_1 = require('./internal/signaling-environment');
|
|
64
65
|
const call_state_controller_1 = require('./internal/calls/call-state-controller');
|
|
65
66
|
const voice_pn_bridge_1 = require('./internal/voice-pn-bridge');
|
|
67
|
+
const ice_servers_storage_1 = require('./internal/ice-servers-storage');
|
|
66
68
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
67
69
|
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
68
70
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
71
|
+
const QUALITY_METRICS_STORAGE_KEY = '@enable_quality_metrics';
|
|
69
72
|
const LEGACY_CALLKIT_ANSWER_KEY = '__legacy_callkit_answer__';
|
|
70
73
|
/**
|
|
71
74
|
* The main public interface for the react-voice-commons module.
|
|
@@ -111,10 +114,13 @@ class TelnyxVoipClient {
|
|
|
111
114
|
enableAppStateManagement: true,
|
|
112
115
|
debug: false,
|
|
113
116
|
useTrickleIce: false,
|
|
117
|
+
environment: 'production',
|
|
114
118
|
...options,
|
|
115
119
|
};
|
|
116
120
|
// Initialize core components
|
|
117
|
-
this._sessionManager = new session_manager_1.SessionManager(
|
|
121
|
+
this._sessionManager = new session_manager_1.SessionManager({
|
|
122
|
+
defaultEnvironment: this._options.environment,
|
|
123
|
+
});
|
|
118
124
|
this._callStateController = new call_state_controller_1.CallStateController(
|
|
119
125
|
this._sessionManager
|
|
120
126
|
);
|
|
@@ -308,6 +314,7 @@ class TelnyxVoipClient {
|
|
|
308
314
|
const loginConfig = await this._withNativeVoipPushToken({
|
|
309
315
|
...config,
|
|
310
316
|
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
317
|
+
environment: config.environment ?? this._options.environment,
|
|
311
318
|
});
|
|
312
319
|
await voice_pn_bridge_1.VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
|
|
313
320
|
// Store credentials for future reconnection
|
|
@@ -335,6 +342,7 @@ class TelnyxVoipClient {
|
|
|
335
342
|
const loginConfig = await this._withNativeVoipPushToken({
|
|
336
343
|
...config,
|
|
337
344
|
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
345
|
+
environment: config.environment ?? this._options.environment,
|
|
338
346
|
});
|
|
339
347
|
await voice_pn_bridge_1.VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
|
|
340
348
|
// Store token for future reconnection
|
|
@@ -362,7 +370,7 @@ class TelnyxVoipClient {
|
|
|
362
370
|
* This method is used for auto-reconnection scenarios where the app
|
|
363
371
|
* comes back to the foreground and needs to restore the connection.
|
|
364
372
|
*
|
|
365
|
-
* @returns
|
|
373
|
+
* @returns Whether reconnection was successful.
|
|
366
374
|
*/
|
|
367
375
|
async loginFromStoredConfig() {
|
|
368
376
|
this._throwIfDisposed();
|
|
@@ -381,10 +389,22 @@ class TelnyxVoipClient {
|
|
|
381
389
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
382
390
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
383
391
|
);
|
|
392
|
+
const storedEnvironment = await AsyncStorage.getItem(
|
|
393
|
+
signaling_environment_1.SIGNALING_ENVIRONMENT_STORAGE_KEY
|
|
394
|
+
);
|
|
395
|
+
const storedQualityMetrics = await AsyncStorage.getItem(QUALITY_METRICS_STORAGE_KEY);
|
|
396
|
+
const iceServers = (0, ice_servers_storage_1.parseStoredIceServers)(
|
|
397
|
+
await AsyncStorage.getItem(ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY)
|
|
398
|
+
);
|
|
384
399
|
const useTrickleIce =
|
|
385
400
|
storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
|
|
386
401
|
const pushWhenActive = storedPushWhenActive === 'true';
|
|
387
402
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
403
|
+
const enableQualityMetrics = storedQualityMetrics === 'true';
|
|
404
|
+
const environment = (0, signaling_environment_1.resolveStoredEnvironment)(
|
|
405
|
+
storedEnvironment,
|
|
406
|
+
this._options.environment
|
|
407
|
+
);
|
|
388
408
|
const nativePushToken =
|
|
389
409
|
react_native_1.Platform.OS === 'ios'
|
|
390
410
|
? (await voice_pn_bridge_1.VoicePnBridge.getVoipToken())?.trim()
|
|
@@ -402,6 +422,9 @@ class TelnyxVoipClient {
|
|
|
402
422
|
useTrickleIce,
|
|
403
423
|
pushWhenActive,
|
|
404
424
|
enableMissedCallNotifications,
|
|
425
|
+
enableQualityMetrics,
|
|
426
|
+
environment,
|
|
427
|
+
iceServers,
|
|
405
428
|
});
|
|
406
429
|
if (this._options.debug) {
|
|
407
430
|
console.log(
|
|
@@ -421,6 +444,9 @@ class TelnyxVoipClient {
|
|
|
421
444
|
useTrickleIce,
|
|
422
445
|
pushWhenActive,
|
|
423
446
|
enableMissedCallNotifications,
|
|
447
|
+
enableQualityMetrics,
|
|
448
|
+
environment,
|
|
449
|
+
iceServers,
|
|
424
450
|
});
|
|
425
451
|
if (this._options.debug) {
|
|
426
452
|
console.log('TelnyxVoipClient: Reconnecting with stored token');
|
|
@@ -682,6 +708,22 @@ class TelnyxVoipClient {
|
|
|
682
708
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
683
709
|
String(config.enableMissedCallNotifications ?? false)
|
|
684
710
|
);
|
|
711
|
+
await AsyncStorage.setItem(
|
|
712
|
+
QUALITY_METRICS_STORAGE_KEY,
|
|
713
|
+
String(config.enableQualityMetrics ?? false)
|
|
714
|
+
);
|
|
715
|
+
await AsyncStorage.setItem(
|
|
716
|
+
signaling_environment_1.SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
717
|
+
config.environment ?? this._options.environment ?? 'production'
|
|
718
|
+
);
|
|
719
|
+
if (config.iceServers) {
|
|
720
|
+
await AsyncStorage.setItem(
|
|
721
|
+
ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY,
|
|
722
|
+
JSON.stringify(config.iceServers)
|
|
723
|
+
);
|
|
724
|
+
} else {
|
|
725
|
+
await AsyncStorage.removeItem(ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY);
|
|
726
|
+
}
|
|
685
727
|
if (config.pushNotificationDeviceToken) {
|
|
686
728
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
687
729
|
}
|
|
@@ -716,6 +758,22 @@ class TelnyxVoipClient {
|
|
|
716
758
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
717
759
|
String(config.enableMissedCallNotifications ?? false)
|
|
718
760
|
);
|
|
761
|
+
await AsyncStorage.setItem(
|
|
762
|
+
QUALITY_METRICS_STORAGE_KEY,
|
|
763
|
+
String(config.enableQualityMetrics ?? false)
|
|
764
|
+
);
|
|
765
|
+
await AsyncStorage.setItem(
|
|
766
|
+
signaling_environment_1.SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
767
|
+
config.environment ?? this._options.environment ?? 'production'
|
|
768
|
+
);
|
|
769
|
+
if (config.iceServers) {
|
|
770
|
+
await AsyncStorage.setItem(
|
|
771
|
+
ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY,
|
|
772
|
+
JSON.stringify(config.iceServers)
|
|
773
|
+
);
|
|
774
|
+
} else {
|
|
775
|
+
await AsyncStorage.removeItem(ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY);
|
|
776
|
+
}
|
|
719
777
|
if (config.pushNotificationDeviceToken) {
|
|
720
778
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
721
779
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
69
69
|
"react": ">=19.0.0 <20.0.0",
|
|
70
70
|
"react-native": ">=0.79.0 <1.0.0",
|
|
71
|
-
"react-native-webrtc": "^
|
|
71
|
+
"react-native-webrtc": "^155.0.1"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@react-native-async-storage/async-storage": {
|
|
@@ -86,9 +86,8 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
89
|
-
"@telnyx/react-native-voice-sdk": "
|
|
89
|
+
"@telnyx/react-native-voice-sdk": "^1.2.0",
|
|
90
90
|
"eventemitter3": "^5.0.1",
|
|
91
|
-
"expo": "~53.0.22",
|
|
92
91
|
"react-native-url-polyfill": "^3.0.0",
|
|
93
92
|
"react-native-voip-push-notification": "^3.3.3",
|
|
94
93
|
"rxjs": "^7.8.2"
|
|
@@ -4,6 +4,14 @@ import { Call } from '@telnyx/react-native-voice-sdk';
|
|
|
4
4
|
import { VoicePnBridge } from '../internal/voice-pn-bridge';
|
|
5
5
|
import type { TelnyxVoipClient } from '../telnyx-voip-client';
|
|
6
6
|
|
|
7
|
+
export type RemoteCallEndedReason = {
|
|
8
|
+
cause?: string;
|
|
9
|
+
causeCode?: number;
|
|
10
|
+
sipCode?: number;
|
|
11
|
+
sipReason?: string;
|
|
12
|
+
sipCallId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
7
15
|
type PendingSwap = {
|
|
8
16
|
mode: 'swap' | 'restoring' | 'rollback';
|
|
9
17
|
originalActiveCallKitUUID: string;
|
|
@@ -37,6 +45,8 @@ class CallKitCoordinator {
|
|
|
37
45
|
// Tracks action+UUID pairs so an end action is not dropped merely because
|
|
38
46
|
// an answer or held action for the same call is still completing.
|
|
39
47
|
private processingCalls = new Set<string>();
|
|
48
|
+
// CallKit/the app owns cross-call hold and swap policy. Only serialize
|
|
49
|
+
// duplicate actions for the same UUID; held calls must not block a new answer.
|
|
40
50
|
|
|
41
51
|
// Tracks calls that have already been ended in CallKit to prevent duplicate reports
|
|
42
52
|
private endedCalls = new Set<string>();
|
|
@@ -744,11 +754,44 @@ class CallKitCoordinator {
|
|
|
744
754
|
}
|
|
745
755
|
}
|
|
746
756
|
|
|
757
|
+
/** End a still-pending PushKit CallKit call after a remote hangup. */
|
|
758
|
+
async handlePendingPushCallRemoteEnded(
|
|
759
|
+
callKitUUID: string,
|
|
760
|
+
reason?: RemoteCallEndedReason
|
|
761
|
+
): Promise<boolean> {
|
|
762
|
+
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
763
|
+
|
|
764
|
+
// Do not let an unrelated/stale socket BYE terminate another CallKit call.
|
|
765
|
+
// The UUID is added here only after native CallKit registration succeeds.
|
|
766
|
+
if (!this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
767
|
+
console.log('CallKitCoordinator: Ignoring remote-ended event for unknown push UUID', {
|
|
768
|
+
callKitUUID,
|
|
769
|
+
reason,
|
|
770
|
+
});
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
console.log('CallKitCoordinator: Ending pending push call after remote hangup', {
|
|
775
|
+
callKitUUID,
|
|
776
|
+
reason,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
if (!this.endedCalls.has(callKitUUID)) {
|
|
780
|
+
await CallKit.reportCallEnded(callKitUUID, CallEndReason.RemoteEnded);
|
|
781
|
+
this.endedCalls.add(callKitUUID);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.cleanupCall(callKitUUID);
|
|
785
|
+
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
786
|
+
await this.checkBackgroundDisconnection();
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
|
|
747
790
|
/**
|
|
748
791
|
* Handle push notification answer - when user answers from CallKit but we don't have a WebRTC call yet
|
|
749
792
|
* This is the iOS equivalent of the Android FCM handler
|
|
750
793
|
*/
|
|
751
|
-
private async handlePushNotificationAnswer(callKitUUID: string, event?: any) {
|
|
794
|
+
private async handlePushNotificationAnswer(callKitUUID: string, event?: any): Promise<boolean> {
|
|
752
795
|
try {
|
|
753
796
|
console.log(
|
|
754
797
|
'CallKitCoordinator: Handling push notification answer for CallKit UUID:',
|
|
@@ -770,7 +813,7 @@ class CallKitCoordinator {
|
|
|
770
813
|
// checkForInitialPushNotification() will run after setVoipClient()
|
|
771
814
|
// and will find the push data still intact, call handleCallKitPushReceived()
|
|
772
815
|
// which queues the matching auto-answer.
|
|
773
|
-
return;
|
|
816
|
+
return false;
|
|
774
817
|
}
|
|
775
818
|
|
|
776
819
|
// Queue one UUID-targeted answer. If the push is already being
|
|
@@ -779,22 +822,22 @@ class CallKitCoordinator {
|
|
|
779
822
|
voipClient.queueAnswerFromCallKit(callKitUUID);
|
|
780
823
|
if (this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
781
824
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
782
|
-
return;
|
|
825
|
+
return true;
|
|
783
826
|
}
|
|
784
827
|
|
|
785
|
-
await this.handleCallKitPushReceived(callKitUUID, event);
|
|
786
|
-
|
|
787
|
-
return;
|
|
828
|
+
return await this.handleCallKitPushReceived(callKitUUID, event);
|
|
788
829
|
}
|
|
789
830
|
|
|
790
831
|
// For other platforms (shouldn't happen on iOS)
|
|
791
832
|
console.error('CallKitCoordinator: ❌ Unsupported platform for push notification handling');
|
|
792
833
|
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
834
|
+
return false;
|
|
793
835
|
} catch (error) {
|
|
794
836
|
console.error('CallKitCoordinator: ❌ Error handling push notification answer:', error);
|
|
795
837
|
// Report the call as failed to CallKit
|
|
796
838
|
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
797
839
|
this.cleanupCall(callKitUUID);
|
|
840
|
+
return false;
|
|
798
841
|
}
|
|
799
842
|
}
|
|
800
843
|
|