@telnyx/react-voice-commons-sdk 1.1.1 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/TelnyxVoiceCommons.podspec +3 -3
  3. package/ios/CallKitBridge.swift +126 -44
  4. package/lib/callkit/callkit-coordinator.d.ts +19 -0
  5. package/lib/callkit/callkit-coordinator.js +105 -5
  6. package/lib/index.d.ts +4 -1
  7. package/lib/index.js +9 -0
  8. package/lib/internal/calls/call-state-controller.d.ts +1 -0
  9. package/lib/internal/calls/call-state-controller.js +35 -1
  10. package/lib/internal/ice-servers-storage.d.ts +9 -0
  11. package/lib/internal/ice-servers-storage.js +37 -0
  12. package/lib/internal/sdk-identity.d.ts +4 -0
  13. package/lib/internal/sdk-identity.js +60 -0
  14. package/lib/internal/session/session-manager.d.ts +7 -2
  15. package/lib/internal/session/session-manager.js +35 -4
  16. package/lib/internal/signaling-environment.d.ts +13 -0
  17. package/lib/internal/signaling-environment.js +18 -0
  18. package/lib/models/call.d.ts +23 -2
  19. package/lib/models/call.js +37 -2
  20. package/lib/models/config.d.ts +28 -0
  21. package/lib/telnyx-voip-client.d.ts +4 -2
  22. package/lib/telnyx-voip-client.js +60 -2
  23. package/package.json +3 -4
  24. package/src/callkit/callkit-coordinator.ts +129 -7
  25. package/src/index.ts +6 -1
  26. package/src/internal/calls/call-state-controller.ts +41 -2
  27. package/src/internal/ice-servers-storage.ts +37 -0
  28. package/src/internal/sdk-identity.ts +6 -0
  29. package/src/internal/session/session-manager.ts +38 -4
  30. package/src/internal/signaling-environment.ts +20 -0
  31. package/src/models/call.ts +44 -2
  32. package/src/models/config.ts +30 -0
  33. package/src/telnyx-voip-client.ts +60 -3
  34. package/src/types/telnyx-sdk.d.ts +67 -2
@@ -31,6 +31,19 @@ class CallStateController {
31
31
  console.log('CallStateController: Reattached call received:', telnyxCall.callId);
32
32
  this._handleIncomingCall(telnyxCall, msg, true);
33
33
  };
34
+ this._handleRemoteCallEnded = (callId, reason) => {
35
+ console.log('CallStateController: Pending push call ended remotely', {
36
+ callId,
37
+ reason,
38
+ });
39
+ // A tracked call can also reach the existing state-listener teardown path.
40
+ // Both paths are intentionally idempotent so pre-INVITE calls are covered too.
41
+ void callkit_coordinator_1.callKitCoordinator
42
+ .handlePendingPushCallRemoteEnded(callId, reason)
43
+ .catch((error) => {
44
+ console.error('CallStateController: Failed to end pending push CallKit call:', error);
45
+ });
46
+ };
34
47
  this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
35
48
  console.log(
36
49
  'CallStateController: Call state changed from TelnyxRTC:',
@@ -184,8 +197,24 @@ class CallStateController {
184
197
  * This should be called by the session manager after client creation
185
198
  */
186
199
  initializeClientListeners() {
200
+ const telnyxClient = this._sessionManager.telnyxClient;
187
201
  console.log('CallStateController: initializeClientListeners called');
188
- console.log('CallStateController: Current client exists:', !!this._sessionManager.telnyxClient);
202
+ console.log('CallStateController: Current client exists:', !!telnyxClient);
203
+ if (!telnyxClient) {
204
+ console.log('CallStateController: No telnyxClient available, skipping listener setup');
205
+ return;
206
+ }
207
+ // Skip redundant listener setup — SessionManager calls _onClientReady
208
+ // twice during connect/reconnect (once before connection, once on
209
+ // telnyx.client.ready event). _setupClientListeners() already
210
+ // removes old listeners before re-adding, so this guard skips the
211
+ // redundant off→on churn rather than preventing actual duplicates.
212
+ if (this._listenerClient === telnyxClient) {
213
+ console.log(
214
+ 'CallStateController: Listeners already registered for this client instance, skipping'
215
+ );
216
+ return;
217
+ }
189
218
  this._setupClientListeners();
190
219
  // CallKit integration now handled by CallKitCoordinator
191
220
  console.log('CallStateController: Using CallKitCoordinator for CallKit integration');
@@ -320,6 +349,10 @@ class CallStateController {
320
349
  telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
321
350
  // Listen for call removal events from TelnyxRTC (multi-call support)
322
351
  telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
352
+ // A caller can hang up while PushKit is reconnecting the socket, before
353
+ // an INVITE creates a Call object. Forward that terminal event so the
354
+ // already-presented CallKit call is dismissed.
355
+ telnyxClient.on('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
323
356
  console.log('CallStateController: Client listeners set up successfully');
324
357
  }
325
358
  _removeClientListeners() {
@@ -330,6 +363,7 @@ class CallStateController {
330
363
  this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
331
364
  this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
332
365
  this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
366
+ this._listenerClient.off('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
333
367
  this._listenerClient = undefined;
334
368
  }
335
369
  /**
@@ -0,0 +1,9 @@
1
+ import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
2
+ export declare const ICE_SERVERS_STORAGE_KEY = '@ice_servers';
3
+ /**
4
+ * Parse the ICE servers persisted by a previous login. A missing, malformed,
5
+ * or structurally invalid value yields `undefined` (the core SDK then uses the
6
+ * Telnyx defaults) rather than throwing, so a bad stored list never discards
7
+ * otherwise valid stored authentication.
8
+ */
9
+ export declare function parseStoredIceServers(stored: string | null): TelnyxIceServer[] | undefined;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.ICE_SERVERS_STORAGE_KEY = void 0;
4
+ exports.parseStoredIceServers = parseStoredIceServers;
5
+ exports.ICE_SERVERS_STORAGE_KEY = '@ice_servers';
6
+ /**
7
+ * Parse the ICE servers persisted by a previous login. A missing, malformed,
8
+ * or structurally invalid value yields `undefined` (the core SDK then uses the
9
+ * Telnyx defaults) rather than throwing, so a bad stored list never discards
10
+ * otherwise valid stored authentication.
11
+ */
12
+ function parseStoredIceServers(stored) {
13
+ if (!stored) {
14
+ return undefined;
15
+ }
16
+ try {
17
+ const parsed = JSON.parse(stored);
18
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isIceServer)) {
19
+ return parsed;
20
+ }
21
+ } catch {
22
+ // Fall through to the warning below.
23
+ }
24
+ console.warn('Ignoring invalid stored ICE servers; using the SDK defaults');
25
+ return undefined;
26
+ }
27
+ function isIceServer(value) {
28
+ if (!value || typeof value !== 'object') {
29
+ return false;
30
+ }
31
+ const server = value;
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
+ }
@@ -0,0 +1,4 @@
1
+ export declare const REACT_NATIVE_COMMONS_SDK_IDENTITY: Readonly<{
2
+ name: 'webrtc-react-native-commons';
3
+ version: string;
4
+ }>;
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = {
10
+ enumerable: true,
11
+ get: function () {
12
+ return m[k];
13
+ },
14
+ };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }
18
+ : function (o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ });
22
+ var __setModuleDefault =
23
+ (this && this.__setModuleDefault) ||
24
+ (Object.create
25
+ ? function (o, v) {
26
+ Object.defineProperty(o, 'default', { enumerable: true, value: v });
27
+ }
28
+ : function (o, v) {
29
+ o['default'] = v;
30
+ });
31
+ var __importStar =
32
+ (this && this.__importStar) ||
33
+ (function () {
34
+ var ownKeys = function (o) {
35
+ ownKeys =
36
+ Object.getOwnPropertyNames ||
37
+ function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null)
48
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++)
49
+ if (k[i] !== 'default') __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, '__esModule', { value: true });
55
+ exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = void 0;
56
+ const pkg = __importStar(require('../../package.json'));
57
+ exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = Object.freeze({
58
+ name: 'webrtc-react-native-commons',
59
+ version: pkg.version,
60
+ });
@@ -1,7 +1,11 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import * as TelnyxSDK from '@telnyx/react-native-voice-sdk';
3
3
  import { TelnyxConnectionState } from '../../models/connection-state';
4
- import { Config, CredentialConfig, TokenConfig } from '../../models/config';
4
+ import { Config, CredentialConfig, TelnyxEnvironment, TokenConfig } from '../../models/config';
5
+ export interface SessionManagerOptions {
6
+ /** Environment used when a config or stored restore does not specify one */
7
+ defaultEnvironment?: TelnyxEnvironment;
8
+ }
5
9
  /**
6
10
  * Manages the connection lifecycle to the Telnyx platform.
7
11
  *
@@ -20,7 +24,8 @@ export declare class SessionManager {
20
24
  private _connectPromise?;
21
25
  private _onClientReady?;
22
26
  private _onDisconnect?;
23
- constructor();
27
+ private readonly _defaultEnvironment;
28
+ constructor(options?: SessionManagerOptions);
24
29
  /**
25
30
  * Observable stream of connection state changes
26
31
  */
@@ -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 pkg = __importStar(require('../../../package.json'));
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
- sdkVersion: pkg.version,
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
- sdkVersion: pkg.version,
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
+ }
@@ -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 should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
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 should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
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
  */
@@ -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 should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
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 should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
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
  */
@@ -433,10 +462,12 @@ class Call {
433
462
  */
434
463
  dispose() {
435
464
  this._stopDurationTimer();
465
+ this._telnyxCall.off?.('telnyx.call.qualityMetrics', this._onQualityMetrics);
436
466
  this._callState.complete();
437
467
  this._isMuted.complete();
438
468
  this._isHeld.complete();
439
469
  this._duration.complete();
470
+ this._qualityMetrics.complete();
440
471
  }
441
472
  /**
442
473
  * Set up listeners for the underlying Telnyx call
@@ -531,6 +562,10 @@ class Call {
531
562
  }
532
563
  }
533
564
  });
565
+ // Forward quality metrics from the underlying SDK
566
+ // Subscribe to the event rather than the single onQualityMetrics slot, so
567
+ // an app can still set that callback on the underlying call.
568
+ this._telnyxCall.on('telnyx.call.qualityMetrics', this._onQualityMetrics);
534
569
  }
535
570
  /**
536
571
  * Map Telnyx SDK call states to our simplified call states
@@ -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 Promise<boolean> - true if reconnection was successful, false otherwise
167
+ * @returns Whether reconnection was successful.
166
168
  */
167
169
  loginFromStoredConfig(): Promise<boolean>;
168
170
  /**