@telnyx/react-voice-commons-sdk 1.1.0 → 1.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/CHANGELOG.md +38 -1
- package/TelnyxVoiceCommons.podspec +3 -3
- package/ios/CallKitBridge.m +7 -0
- package/ios/CallKitBridge.swift +239 -91
- package/lib/callkit/callkit-coordinator.d.ts +19 -0
- package/lib/callkit/callkit-coordinator.js +105 -5
- 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 +37 -2
- 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 +129 -7
- 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 +44 -2
- package/src/models/config.ts +30 -0
- package/src/telnyx-voip-client.ts +60 -3
- package/src/types/telnyx-sdk.d.ts +67 -2
|
@@ -4,7 +4,7 @@ import { TelnyxRTC, Call as TelnyxCall, type CallOptions } from '@telnyx/react-n
|
|
|
4
4
|
import { Call } from '../../models/call';
|
|
5
5
|
import { TelnyxCallState } from '../../models/call-state';
|
|
6
6
|
import { SessionManager } from '../session/session-manager';
|
|
7
|
-
import { callKitCoordinator } from '../../callkit/callkit-coordinator';
|
|
7
|
+
import { callKitCoordinator, type RemoteCallEndedReason } from '../../callkit/callkit-coordinator';
|
|
8
8
|
|
|
9
9
|
export type CustomHeader = { name: string; value: string };
|
|
10
10
|
export type CustomHeaders = Record<string, string> | CustomHeader[];
|
|
@@ -178,8 +178,28 @@ export class CallStateController {
|
|
|
178
178
|
* This should be called by the session manager after client creation
|
|
179
179
|
*/
|
|
180
180
|
initializeClientListeners(): void {
|
|
181
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
182
|
+
|
|
181
183
|
console.log('CallStateController: initializeClientListeners called');
|
|
182
|
-
console.log('CallStateController: Current client exists:', !!
|
|
184
|
+
console.log('CallStateController: Current client exists:', !!telnyxClient);
|
|
185
|
+
|
|
186
|
+
if (!telnyxClient) {
|
|
187
|
+
console.log('CallStateController: No telnyxClient available, skipping listener setup');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Skip redundant listener setup — SessionManager calls _onClientReady
|
|
192
|
+
// twice during connect/reconnect (once before connection, once on
|
|
193
|
+
// telnyx.client.ready event). _setupClientListeners() already
|
|
194
|
+
// removes old listeners before re-adding, so this guard skips the
|
|
195
|
+
// redundant off→on churn rather than preventing actual duplicates.
|
|
196
|
+
if (this._listenerClient === telnyxClient) {
|
|
197
|
+
console.log(
|
|
198
|
+
'CallStateController: Listeners already registered for this client instance, skipping'
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
this._setupClientListeners();
|
|
184
204
|
|
|
185
205
|
// CallKit integration now handled by CallKitCoordinator
|
|
@@ -352,6 +372,11 @@ export class CallStateController {
|
|
|
352
372
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
353
373
|
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
354
374
|
|
|
375
|
+
// A caller can hang up while PushKit is reconnecting the socket, before
|
|
376
|
+
// an INVITE creates a Call object. Forward that terminal event so the
|
|
377
|
+
// already-presented CallKit call is dismissed.
|
|
378
|
+
telnyxClient.on('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
379
|
+
|
|
355
380
|
console.log('CallStateController: Client listeners set up successfully');
|
|
356
381
|
}
|
|
357
382
|
|
|
@@ -364,6 +389,7 @@ export class CallStateController {
|
|
|
364
389
|
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
365
390
|
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
366
391
|
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
392
|
+
this._listenerClient.off('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
367
393
|
this._listenerClient = undefined;
|
|
368
394
|
}
|
|
369
395
|
|
|
@@ -377,6 +403,19 @@ export class CallStateController {
|
|
|
377
403
|
this._handleIncomingCall(telnyxCall, msg, true);
|
|
378
404
|
};
|
|
379
405
|
|
|
406
|
+
private _handleRemoteCallEnded = (callId: string, reason: RemoteCallEndedReason): void => {
|
|
407
|
+
console.log('CallStateController: Pending push call ended remotely', {
|
|
408
|
+
callId,
|
|
409
|
+
reason,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// A tracked call can also reach the existing state-listener teardown path.
|
|
413
|
+
// Both paths are intentionally idempotent so pre-INVITE calls are covered too.
|
|
414
|
+
void callKitCoordinator.handlePendingPushCallRemoteEnded(callId, reason).catch((error) => {
|
|
415
|
+
console.error('CallStateController: Failed to end pending push CallKit call:', error);
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
|
|
380
419
|
private _handleTelnyxCallStateChanged = (telnyxCall: TelnyxCall, state: string): void => {
|
|
381
420
|
console.log(
|
|
382
421
|
'CallStateController: Call state changed from TelnyxRTC:',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
2
|
+
|
|
3
|
+
export const ICE_SERVERS_STORAGE_KEY = '@ice_servers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse the ICE servers persisted by a previous login. A missing, malformed,
|
|
7
|
+
* or structurally invalid value yields `undefined` (the core SDK then uses the
|
|
8
|
+
* Telnyx defaults) rather than throwing, so a bad stored list never discards
|
|
9
|
+
* otherwise valid stored authentication.
|
|
10
|
+
*/
|
|
11
|
+
export function parseStoredIceServers(stored: string | null): TelnyxIceServer[] | undefined {
|
|
12
|
+
if (!stored) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const parsed: unknown = JSON.parse(stored);
|
|
17
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isIceServer)) {
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// Fall through to the warning below.
|
|
22
|
+
}
|
|
23
|
+
console.warn('Ignoring invalid stored ICE servers; using the SDK defaults');
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isIceServer(value: unknown): value is TelnyxIceServer {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const server = value as Record<string, unknown>;
|
|
32
|
+
return (
|
|
33
|
+
typeof server.urls === 'string' &&
|
|
34
|
+
(server.username === undefined || typeof server.username === 'string') &&
|
|
35
|
+
(server.credential === undefined || typeof server.credential === 'string')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { BehaviorSubject, Observable } from 'rxjs';
|
|
2
2
|
import { distinctUntilChanged } from 'rxjs/operators';
|
|
3
3
|
import * as TelnyxSDK from '@telnyx/react-native-voice-sdk';
|
|
4
|
-
import
|
|
4
|
+
import { REACT_NATIVE_COMMONS_SDK_IDENTITY } from '../sdk-identity';
|
|
5
|
+
import { ICE_SERVERS_STORAGE_KEY, parseStoredIceServers } from '../ice-servers-storage';
|
|
6
|
+
import {
|
|
7
|
+
SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
8
|
+
resolveStoredEnvironment,
|
|
9
|
+
} from '../signaling-environment';
|
|
5
10
|
import { TelnyxConnectionState } from '../../models/connection-state';
|
|
6
11
|
import {
|
|
7
12
|
Config,
|
|
8
13
|
CredentialConfig,
|
|
14
|
+
TelnyxEnvironment,
|
|
9
15
|
TokenConfig,
|
|
10
16
|
isCredentialConfig,
|
|
11
17
|
isTokenConfig,
|
|
@@ -14,6 +20,12 @@ import {
|
|
|
14
20
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
15
21
|
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
16
22
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
23
|
+
const QUALITY_METRICS_STORAGE_KEY = '@enable_quality_metrics';
|
|
24
|
+
|
|
25
|
+
export interface SessionManagerOptions {
|
|
26
|
+
/** Environment used when a config or stored restore does not specify one */
|
|
27
|
+
defaultEnvironment?: TelnyxEnvironment;
|
|
28
|
+
}
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
@@ -35,9 +47,11 @@ export class SessionManager {
|
|
|
35
47
|
private _connectPromise?: Promise<void>;
|
|
36
48
|
private _onClientReady?: () => void;
|
|
37
49
|
private _onDisconnect?: () => void;
|
|
50
|
+
private readonly _defaultEnvironment: TelnyxEnvironment;
|
|
38
51
|
|
|
39
|
-
constructor() {
|
|
52
|
+
constructor(options: SessionManagerOptions = {}) {
|
|
40
53
|
this._sessionId = this._generateSessionId();
|
|
54
|
+
this._defaultEnvironment = options.defaultEnvironment ?? 'production';
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -243,9 +257,15 @@ export class SessionManager {
|
|
|
243
257
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
244
258
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
245
259
|
);
|
|
260
|
+
const storedEnvironment = await AsyncStorage.getItem(SIGNALING_ENVIRONMENT_STORAGE_KEY);
|
|
261
|
+
const storedQualityMetrics = await AsyncStorage.getItem(QUALITY_METRICS_STORAGE_KEY);
|
|
262
|
+
const storedIceServers = await AsyncStorage.getItem(ICE_SERVERS_STORAGE_KEY);
|
|
263
|
+
const iceServers = parseStoredIceServers(storedIceServers);
|
|
246
264
|
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
247
265
|
const pushWhenActive = storedPushWhenActive === 'true';
|
|
248
266
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
267
|
+
const enableQualityMetrics = storedQualityMetrics === 'true';
|
|
268
|
+
const environment = resolveStoredEnvironment(storedEnvironment, this._defaultEnvironment);
|
|
249
269
|
|
|
250
270
|
if (this._isTeardownActive()) {
|
|
251
271
|
return;
|
|
@@ -260,6 +280,9 @@ export class SessionManager {
|
|
|
260
280
|
useTrickleIce,
|
|
261
281
|
pushWhenActive,
|
|
262
282
|
enableMissedCallNotifications,
|
|
283
|
+
enableQualityMetrics,
|
|
284
|
+
environment,
|
|
285
|
+
iceServers,
|
|
263
286
|
});
|
|
264
287
|
}
|
|
265
288
|
// Check if we have token-based authentication data
|
|
@@ -271,6 +294,9 @@ export class SessionManager {
|
|
|
271
294
|
useTrickleIce,
|
|
272
295
|
pushWhenActive,
|
|
273
296
|
enableMissedCallNotifications,
|
|
297
|
+
enableQualityMetrics,
|
|
298
|
+
environment,
|
|
299
|
+
iceServers,
|
|
274
300
|
});
|
|
275
301
|
}
|
|
276
302
|
|
|
@@ -485,11 +511,15 @@ export class SessionManager {
|
|
|
485
511
|
pushWhenActive: config.pushWhenActive,
|
|
486
512
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
487
513
|
useTrickleIce: config.useTrickleIce,
|
|
514
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
488
515
|
enableCallReports: config.enableCallReports,
|
|
516
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
489
517
|
callReportInterval: config.callReportInterval,
|
|
490
518
|
callReportLogLevel: config.callReportLogLevel,
|
|
491
519
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
492
|
-
|
|
520
|
+
iceServers: config.iceServers,
|
|
521
|
+
sdkName: REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
522
|
+
sdkVersion: REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
493
523
|
};
|
|
494
524
|
console.log(
|
|
495
525
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
@@ -506,11 +536,15 @@ export class SessionManager {
|
|
|
506
536
|
pushWhenActive: config.pushWhenActive,
|
|
507
537
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
508
538
|
useTrickleIce: config.useTrickleIce,
|
|
539
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
509
540
|
enableCallReports: config.enableCallReports,
|
|
541
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
510
542
|
callReportInterval: config.callReportInterval,
|
|
511
543
|
callReportLogLevel: config.callReportLogLevel,
|
|
512
544
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
513
|
-
|
|
545
|
+
iceServers: config.iceServers,
|
|
546
|
+
sdkName: REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
547
|
+
sdkVersion: REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
514
548
|
};
|
|
515
549
|
console.log(
|
|
516
550
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TelnyxEnvironment } from '../models/config';
|
|
2
|
+
|
|
3
|
+
export const SIGNALING_ENVIRONMENT_STORAGE_KEY = '@telnyx_signaling_environment';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the signaling environment for a stored-config restore.
|
|
7
|
+
*
|
|
8
|
+
* An explicit stored selection always wins. A missing or invalid stored value
|
|
9
|
+
* (e.g. credentials persisted by an older Commons version) falls back to the
|
|
10
|
+
* client's configured default, and only then to production.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveStoredEnvironment(
|
|
13
|
+
storedEnvironment: string | null | undefined,
|
|
14
|
+
defaultEnvironment?: TelnyxEnvironment
|
|
15
|
+
): TelnyxEnvironment {
|
|
16
|
+
if (storedEnvironment === 'development' || storedEnvironment === 'production') {
|
|
17
|
+
return storedEnvironment;
|
|
18
|
+
}
|
|
19
|
+
return defaultEnvironment ?? 'production';
|
|
20
|
+
}
|
package/src/models/call.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
|
|
2
2
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
|
3
3
|
import { TelnyxCallState, CallStateHelpers } from './call-state';
|
|
4
4
|
import { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
5
|
+
import type { CallQualityMetrics } from '@telnyx/react-native-voice-sdk';
|
|
5
6
|
import { Platform } from 'react-native';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -16,6 +17,10 @@ export class Call {
|
|
|
16
17
|
private readonly _isMuted = new BehaviorSubject<boolean>(false);
|
|
17
18
|
private readonly _isHeld = new BehaviorSubject<boolean>(false);
|
|
18
19
|
private readonly _duration = new BehaviorSubject<number>(0);
|
|
20
|
+
private readonly _qualityMetrics = new BehaviorSubject<CallQualityMetrics | null>(null);
|
|
21
|
+
private readonly _onQualityMetrics = (metrics: CallQualityMetrics) => {
|
|
22
|
+
this._qualityMetrics.next(metrics);
|
|
23
|
+
};
|
|
19
24
|
|
|
20
25
|
private _durationTimer?: NodeJS.Timeout;
|
|
21
26
|
private _startTime?: Date;
|
|
@@ -113,7 +118,7 @@ export class Call {
|
|
|
113
118
|
/**
|
|
114
119
|
* Custom headers received from the WebRTC INVITE message.
|
|
115
120
|
* These headers are passed during call initiation and can contain application-specific information.
|
|
116
|
-
* Format
|
|
121
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
117
122
|
*/
|
|
118
123
|
get inviteCustomHeaders(): { name: string; value: string }[] | null {
|
|
119
124
|
return this._telnyxCall.inviteCustomHeaders;
|
|
@@ -122,7 +127,7 @@ export class Call {
|
|
|
122
127
|
/**
|
|
123
128
|
* Custom headers received from the WebRTC ANSWER message.
|
|
124
129
|
* These headers are passed during call acceptance and can contain application-specific information.
|
|
125
|
-
* Format
|
|
130
|
+
* Format: `[{"name": "X-Header-Name", "value": "Value"}]`; header names must start with `X-`.
|
|
126
131
|
*/
|
|
127
132
|
get answerCustomHeaders(): { name: string; value: string }[] | null {
|
|
128
133
|
return this._telnyxCall.answerCustomHeaders;
|
|
@@ -136,6 +141,19 @@ export class Call {
|
|
|
136
141
|
return this._telnyxCall;
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Inject an ICE failure into the underlying SDK recovery flow for manual
|
|
146
|
+
* testing. The underlying SDK accepts this only when debug mode is enabled.
|
|
147
|
+
*
|
|
148
|
+
* @returns true when a recovery episode was started.
|
|
149
|
+
*/
|
|
150
|
+
simulateIceFailureForTesting(): boolean {
|
|
151
|
+
const testableCall = this._telnyxCall as TelnyxCall & {
|
|
152
|
+
simulateIceFailureForTesting?: () => boolean;
|
|
153
|
+
};
|
|
154
|
+
return testableCall.simulateIceFailureForTesting?.() ?? false;
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
/**
|
|
140
158
|
* Observable stream of call state changes
|
|
141
159
|
*/
|
|
@@ -164,6 +182,23 @@ export class Call {
|
|
|
164
182
|
return this._duration.asObservable().pipe(distinctUntilChanged());
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Observable stream of call quality metrics updates.
|
|
187
|
+
* Emits `null` initially, then a {@link CallQualityMetrics} object
|
|
188
|
+
* each time the underlying SDK collects a new sample.
|
|
189
|
+
*/
|
|
190
|
+
get qualityMetrics$(): Observable<CallQualityMetrics | null> {
|
|
191
|
+
return this._qualityMetrics.asObservable().pipe(distinctUntilChanged());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Current call quality metrics (synchronous access).
|
|
196
|
+
* Returns `null` until the first metrics sample is collected.
|
|
197
|
+
*/
|
|
198
|
+
get currentQualityMetrics(): CallQualityMetrics | null {
|
|
199
|
+
return this._qualityMetrics.value;
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
/**
|
|
168
203
|
* Observable that emits true when the call can be answered
|
|
169
204
|
*/
|
|
@@ -413,10 +448,12 @@ export class Call {
|
|
|
413
448
|
*/
|
|
414
449
|
dispose(): void {
|
|
415
450
|
this._stopDurationTimer();
|
|
451
|
+
this._telnyxCall.off?.('telnyx.call.qualityMetrics', this._onQualityMetrics);
|
|
416
452
|
this._callState.complete();
|
|
417
453
|
this._isMuted.complete();
|
|
418
454
|
this._isHeld.complete();
|
|
419
455
|
this._duration.complete();
|
|
456
|
+
this._qualityMetrics.complete();
|
|
420
457
|
}
|
|
421
458
|
|
|
422
459
|
/**
|
|
@@ -518,6 +555,11 @@ export class Call {
|
|
|
518
555
|
}
|
|
519
556
|
}
|
|
520
557
|
});
|
|
558
|
+
|
|
559
|
+
// Forward quality metrics from the underlying SDK
|
|
560
|
+
// Subscribe to the event rather than the single onQualityMetrics slot, so
|
|
561
|
+
// an app can still set that callback on the underlying call.
|
|
562
|
+
this._telnyxCall.on('telnyx.call.qualityMetrics', this._onQualityMetrics);
|
|
521
563
|
}
|
|
522
564
|
|
|
523
565
|
/**
|
package/src/models/config.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Configuration for credential-based authentication
|
|
3
5
|
*/
|
|
6
|
+
export type TelnyxEnvironment = 'production' | 'development';
|
|
7
|
+
|
|
4
8
|
export interface CredentialConfig {
|
|
5
9
|
type: 'credential';
|
|
6
10
|
sipUser: string;
|
|
@@ -15,14 +19,27 @@ export interface CredentialConfig {
|
|
|
15
19
|
incomingCallRingtone?: string;
|
|
16
20
|
/** Enable Trickle ICE. Default: false */
|
|
17
21
|
useTrickleIce?: boolean;
|
|
22
|
+
/** Signaling environment. Default: production */
|
|
23
|
+
environment?: TelnyxEnvironment;
|
|
18
24
|
/** Enable automatic call quality reporting. Default: true */
|
|
19
25
|
enableCallReports?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Deliver real-time call quality metrics on `Call.qualityMetrics$` while a
|
|
28
|
+
* call is active. Also enabled by `debug`. Default: false
|
|
29
|
+
*/
|
|
30
|
+
enableQualityMetrics?: boolean;
|
|
20
31
|
/** Stats collection interval in seconds. Default: 5 */
|
|
21
32
|
callReportInterval?: number;
|
|
22
33
|
/** Minimum log level for call reports: 'debug' | 'info' | 'warn' | 'error'. Default: 'debug' */
|
|
23
34
|
callReportLogLevel?: string;
|
|
24
35
|
/** Max log entries per call. Default: 1000 */
|
|
25
36
|
callReportMaxLogEntries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Custom ICE servers (STUN/TURN) for WebRTC peer connections.
|
|
39
|
+
* When provided, overrides the Telnyx production defaults.
|
|
40
|
+
* When omitted, the SDK uses Telnyx defaults (STUN → TURN UDP → TURN TCP → TURNS 443).
|
|
41
|
+
*/
|
|
42
|
+
iceServers?: TelnyxIceServer[];
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
/**
|
|
@@ -41,14 +58,27 @@ export interface TokenConfig {
|
|
|
41
58
|
incomingCallRingtone?: string;
|
|
42
59
|
/** Enable Trickle ICE. Default: false */
|
|
43
60
|
useTrickleIce?: boolean;
|
|
61
|
+
/** Signaling environment. Default: production */
|
|
62
|
+
environment?: TelnyxEnvironment;
|
|
44
63
|
/** Enable automatic call quality reporting. Default: true */
|
|
45
64
|
enableCallReports?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Deliver real-time call quality metrics on `Call.qualityMetrics$` while a
|
|
67
|
+
* call is active. Also enabled by `debug`. Default: false
|
|
68
|
+
*/
|
|
69
|
+
enableQualityMetrics?: boolean;
|
|
46
70
|
/** Stats collection interval in seconds. Default: 5 */
|
|
47
71
|
callReportInterval?: number;
|
|
48
72
|
/** Minimum log level for call reports: 'debug' | 'info' | 'warn' | 'error'. Default: 'debug' */
|
|
49
73
|
callReportLogLevel?: string;
|
|
50
74
|
/** Max log entries per call. Default: 1000 */
|
|
51
75
|
callReportMaxLogEntries?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Custom ICE servers (STUN/TURN) for WebRTC peer connections.
|
|
78
|
+
* When provided, overrides the Telnyx production defaults.
|
|
79
|
+
* When omitted, the SDK uses Telnyx defaults (STUN → TURN UDP → TURN TCP → TURNS 443).
|
|
80
|
+
*/
|
|
81
|
+
iceServers?: TelnyxIceServer[];
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
/**
|
|
@@ -3,14 +3,26 @@ import { Platform } from 'react-native';
|
|
|
3
3
|
import { TelnyxConnectionState } from './models/connection-state';
|
|
4
4
|
import { Call } from './models/call';
|
|
5
5
|
import { TelnyxCallState } from './models/call-state';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Config,
|
|
8
|
+
CredentialConfig,
|
|
9
|
+
TelnyxEnvironment,
|
|
10
|
+
TokenConfig,
|
|
11
|
+
validateConfig,
|
|
12
|
+
} from './models/config';
|
|
7
13
|
import { SessionManager } from './internal/session/session-manager';
|
|
14
|
+
import {
|
|
15
|
+
SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
16
|
+
resolveStoredEnvironment,
|
|
17
|
+
} from './internal/signaling-environment';
|
|
8
18
|
import { CallStateController, type CustomHeaders } from './internal/calls/call-state-controller';
|
|
9
19
|
import { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
20
|
+
import { ICE_SERVERS_STORAGE_KEY, parseStoredIceServers } from './internal/ice-servers-storage';
|
|
10
21
|
|
|
11
22
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
12
23
|
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
13
24
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
25
|
+
const QUALITY_METRICS_STORAGE_KEY = '@enable_quality_metrics';
|
|
14
26
|
const LEGACY_CALLKIT_ANSWER_KEY = '__legacy_callkit_answer__';
|
|
15
27
|
|
|
16
28
|
interface PendingCallKitAnswer {
|
|
@@ -30,6 +42,9 @@ export interface TelnyxVoipClientOptions {
|
|
|
30
42
|
|
|
31
43
|
/** Enable Trickle ICE for calls created by this client */
|
|
32
44
|
useTrickleIce?: boolean;
|
|
45
|
+
|
|
46
|
+
/** Signaling environment used for connections - default: production */
|
|
47
|
+
environment?: TelnyxEnvironment;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/**
|
|
@@ -84,11 +99,12 @@ export class TelnyxVoipClient {
|
|
|
84
99
|
enableAppStateManagement: true,
|
|
85
100
|
debug: false,
|
|
86
101
|
useTrickleIce: false,
|
|
102
|
+
environment: 'production',
|
|
87
103
|
...options,
|
|
88
104
|
};
|
|
89
105
|
|
|
90
106
|
// Initialize core components
|
|
91
|
-
this._sessionManager = new SessionManager();
|
|
107
|
+
this._sessionManager = new SessionManager({ defaultEnvironment: this._options.environment });
|
|
92
108
|
this._callStateController = new CallStateController(this._sessionManager);
|
|
93
109
|
|
|
94
110
|
// Set up callback to initialize call state controller listeners when client is ready
|
|
@@ -304,6 +320,7 @@ export class TelnyxVoipClient {
|
|
|
304
320
|
const loginConfig = await this._withNativeVoipPushToken({
|
|
305
321
|
...config,
|
|
306
322
|
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
323
|
+
environment: config.environment ?? this._options.environment,
|
|
307
324
|
});
|
|
308
325
|
|
|
309
326
|
await VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
|
|
@@ -338,6 +355,7 @@ export class TelnyxVoipClient {
|
|
|
338
355
|
const loginConfig = await this._withNativeVoipPushToken({
|
|
339
356
|
...config,
|
|
340
357
|
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
358
|
+
environment: config.environment ?? this._options.environment,
|
|
341
359
|
});
|
|
342
360
|
|
|
343
361
|
await VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
|
|
@@ -372,7 +390,7 @@ export class TelnyxVoipClient {
|
|
|
372
390
|
* This method is used for auto-reconnection scenarios where the app
|
|
373
391
|
* comes back to the foreground and needs to restore the connection.
|
|
374
392
|
*
|
|
375
|
-
* @returns
|
|
393
|
+
* @returns Whether reconnection was successful.
|
|
376
394
|
*/
|
|
377
395
|
async loginFromStoredConfig(): Promise<boolean> {
|
|
378
396
|
this._throwIfDisposed();
|
|
@@ -394,10 +412,15 @@ export class TelnyxVoipClient {
|
|
|
394
412
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
395
413
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
396
414
|
);
|
|
415
|
+
const storedEnvironment = await AsyncStorage.getItem(SIGNALING_ENVIRONMENT_STORAGE_KEY);
|
|
416
|
+
const storedQualityMetrics = await AsyncStorage.getItem(QUALITY_METRICS_STORAGE_KEY);
|
|
417
|
+
const iceServers = parseStoredIceServers(await AsyncStorage.getItem(ICE_SERVERS_STORAGE_KEY));
|
|
397
418
|
const useTrickleIce =
|
|
398
419
|
storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
|
|
399
420
|
const pushWhenActive = storedPushWhenActive === 'true';
|
|
400
421
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
422
|
+
const enableQualityMetrics = storedQualityMetrics === 'true';
|
|
423
|
+
const environment = resolveStoredEnvironment(storedEnvironment, this._options.environment);
|
|
401
424
|
const nativePushToken =
|
|
402
425
|
Platform.OS === 'ios' ? (await VoicePnBridge.getVoipToken())?.trim() : null;
|
|
403
426
|
const pushNotificationDeviceToken = nativePushToken || storedPushToken;
|
|
@@ -415,6 +438,9 @@ export class TelnyxVoipClient {
|
|
|
415
438
|
useTrickleIce,
|
|
416
439
|
pushWhenActive,
|
|
417
440
|
enableMissedCallNotifications,
|
|
441
|
+
enableQualityMetrics,
|
|
442
|
+
environment,
|
|
443
|
+
iceServers,
|
|
418
444
|
});
|
|
419
445
|
|
|
420
446
|
if (this._options.debug) {
|
|
@@ -437,6 +463,9 @@ export class TelnyxVoipClient {
|
|
|
437
463
|
useTrickleIce,
|
|
438
464
|
pushWhenActive,
|
|
439
465
|
enableMissedCallNotifications,
|
|
466
|
+
enableQualityMetrics,
|
|
467
|
+
environment,
|
|
468
|
+
iceServers,
|
|
440
469
|
});
|
|
441
470
|
|
|
442
471
|
if (this._options.debug) {
|
|
@@ -750,6 +779,20 @@ export class TelnyxVoipClient {
|
|
|
750
779
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
751
780
|
String(config.enableMissedCallNotifications ?? false)
|
|
752
781
|
);
|
|
782
|
+
await AsyncStorage.setItem(
|
|
783
|
+
QUALITY_METRICS_STORAGE_KEY,
|
|
784
|
+
String(config.enableQualityMetrics ?? false)
|
|
785
|
+
);
|
|
786
|
+
await AsyncStorage.setItem(
|
|
787
|
+
SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
788
|
+
config.environment ?? this._options.environment ?? 'production'
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
if (config.iceServers) {
|
|
792
|
+
await AsyncStorage.setItem(ICE_SERVERS_STORAGE_KEY, JSON.stringify(config.iceServers));
|
|
793
|
+
} else {
|
|
794
|
+
await AsyncStorage.removeItem(ICE_SERVERS_STORAGE_KEY);
|
|
795
|
+
}
|
|
753
796
|
|
|
754
797
|
if (config.pushNotificationDeviceToken) {
|
|
755
798
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
@@ -789,6 +832,20 @@ export class TelnyxVoipClient {
|
|
|
789
832
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
790
833
|
String(config.enableMissedCallNotifications ?? false)
|
|
791
834
|
);
|
|
835
|
+
await AsyncStorage.setItem(
|
|
836
|
+
QUALITY_METRICS_STORAGE_KEY,
|
|
837
|
+
String(config.enableQualityMetrics ?? false)
|
|
838
|
+
);
|
|
839
|
+
await AsyncStorage.setItem(
|
|
840
|
+
SIGNALING_ENVIRONMENT_STORAGE_KEY,
|
|
841
|
+
config.environment ?? this._options.environment ?? 'production'
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
if (config.iceServers) {
|
|
845
|
+
await AsyncStorage.setItem(ICE_SERVERS_STORAGE_KEY, JSON.stringify(config.iceServers));
|
|
846
|
+
} else {
|
|
847
|
+
await AsyncStorage.removeItem(ICE_SERVERS_STORAGE_KEY);
|
|
848
|
+
}
|
|
792
849
|
|
|
793
850
|
if (config.pushNotificationDeviceToken) {
|
|
794
851
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|