@telnyx/react-voice-commons-sdk 0.4.3 → 1.0.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.
@@ -17,6 +17,7 @@ export class CallStateController {
17
17
  private readonly _calls = new BehaviorSubject<Call[]>([]);
18
18
  private readonly _callMap = new Map<string, Call>();
19
19
  private _disposed = false;
20
+ private _listenerClient?: TelnyxRTC;
20
21
 
21
22
  // Callbacks for waiting for invite logic (used for push notifications)
22
23
  private _isWaitingForInvite?: () => boolean;
@@ -155,6 +156,9 @@ export class CallStateController {
155
156
  callerIdName: callerName,
156
157
  callerIdNumber: callerNumber,
157
158
  customHeaders,
159
+ peerConnectionOptions: {
160
+ useTrickleIce: this._sessionManager.useTrickleIce,
161
+ },
158
162
  };
159
163
  const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
160
164
 
@@ -225,6 +229,7 @@ export class CallStateController {
225
229
  }
226
230
 
227
231
  this._disposed = true;
232
+ this._removeClientListeners();
228
233
 
229
234
  // Dispose of all calls
230
235
  this.currentCalls.forEach((call) => call.dispose());
@@ -241,40 +246,28 @@ export class CallStateController {
241
246
  private _setupClientListeners(): void {
242
247
  console.log('CallStateController: Setting up client listeners...');
243
248
 
244
- if (!this._sessionManager.telnyxClient) {
249
+ const telnyxClient = this._sessionManager.telnyxClient;
250
+
251
+ if (!telnyxClient) {
245
252
  console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
246
253
  return;
247
254
  }
248
255
 
256
+ this._removeClientListeners();
257
+ this._listenerClient = telnyxClient;
258
+
249
259
  console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
250
- console.log(
251
- 'CallStateController: Client instance:',
252
- this._sessionManager.telnyxClient.constructor.name
253
- );
260
+ console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
254
261
 
255
262
  // Listen for incoming calls
256
- this._sessionManager.telnyxClient.on(
257
- 'telnyx.call.incoming',
258
- (telnyxCall: TelnyxCall, msg: any) => {
259
- console.log('CallStateController: Incoming call received:', telnyxCall.callId);
260
- this._handleIncomingCall(telnyxCall, msg, false);
261
- }
262
- );
263
+ telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
263
264
 
264
265
  // Listen for reattached calls (after network reconnection)
265
- this._sessionManager.telnyxClient.on(
266
- 'telnyx.call.reattached',
267
- (telnyxCall: TelnyxCall, msg: any) => {
268
- console.log('CallStateController: Reattached call received:', telnyxCall.callId);
269
- this._handleIncomingCall(telnyxCall, msg, true);
270
- }
271
- );
266
+ telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
272
267
 
273
268
  // Verify listeners are set up
274
- const incomingListeners =
275
- this._sessionManager.telnyxClient.listenerCount('telnyx.call.incoming');
276
- const reattachedListeners =
277
- this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
269
+ const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
270
+ const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
278
271
  console.log(
279
272
  'CallStateController: Listeners registered - incoming:',
280
273
  incomingListeners,
@@ -283,34 +276,57 @@ export class CallStateController {
283
276
  );
284
277
 
285
278
  // Listen for call state changes from the TelnyxRTC client (multi-call support)
286
- this._sessionManager.telnyxClient.on(
287
- 'telnyx.call.stateChanged',
288
- (telnyxCall: TelnyxCall, state: string) => {
289
- console.log(
290
- 'CallStateController: Call state changed from TelnyxRTC:',
291
- telnyxCall.callId,
292
- state
293
- );
294
- // Find our wrapper call and update if needed
295
- const call = this.findCallByTelnyxCall(telnyxCall);
296
- if (call) {
297
- console.log(
298
- 'CallStateController: Found wrapper call, state sync handled by Call subscription'
299
- );
300
- }
301
- }
302
- );
279
+ telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
303
280
 
304
281
  // Listen for call removal events from TelnyxRTC (multi-call support)
305
- this._sessionManager.telnyxClient.on('telnyx.call.removed', (callId: string) => {
306
- console.log('CallStateController: Call removed from TelnyxRTC:', callId);
307
- // The call cleanup is already handled by our call state subscription
308
- // This event is informational for logging/debugging
309
- });
282
+ telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
310
283
 
311
284
  console.log('CallStateController: Client listeners set up successfully');
312
285
  }
313
286
 
287
+ private _removeClientListeners(): void {
288
+ if (!this._listenerClient) {
289
+ return;
290
+ }
291
+
292
+ this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
293
+ this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
294
+ this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
295
+ this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
296
+ this._listenerClient = undefined;
297
+ }
298
+
299
+ private _handleTelnyxIncomingCall = (telnyxCall: TelnyxCall, msg: any): void => {
300
+ console.log('CallStateController: Incoming call received:', telnyxCall.callId);
301
+ this._handleIncomingCall(telnyxCall, msg, false);
302
+ };
303
+
304
+ private _handleTelnyxReattachedCall = (telnyxCall: TelnyxCall, msg: any): void => {
305
+ console.log('CallStateController: Reattached call received:', telnyxCall.callId);
306
+ this._handleIncomingCall(telnyxCall, msg, true);
307
+ };
308
+
309
+ private _handleTelnyxCallStateChanged = (telnyxCall: TelnyxCall, state: string): void => {
310
+ console.log(
311
+ 'CallStateController: Call state changed from TelnyxRTC:',
312
+ telnyxCall.callId,
313
+ state
314
+ );
315
+ // Find our wrapper call and update if needed
316
+ const call = this.findCallByTelnyxCall(telnyxCall);
317
+ if (call) {
318
+ console.log(
319
+ 'CallStateController: Found wrapper call, state sync handled by Call subscription'
320
+ );
321
+ }
322
+ };
323
+
324
+ private _handleTelnyxCallRemoved = (callId: string): void => {
325
+ console.log('CallStateController: Call removed from TelnyxRTC:', callId);
326
+ // The call cleanup is already handled by our call state subscription.
327
+ // This event is informational for logging/debugging.
328
+ };
329
+
314
330
  /**
315
331
  * Handle incoming call or reattached call
316
332
  */
@@ -11,6 +11,9 @@ import {
11
11
  isTokenConfig,
12
12
  } from '../../models/config';
13
13
 
14
+ const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
15
+ const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
16
+
14
17
  /**
15
18
  * Manages the connection lifecycle to the Telnyx platform.
16
19
  *
@@ -75,6 +78,10 @@ export class SessionManager {
75
78
  return this._telnyxClient;
76
79
  }
77
80
 
81
+ get useTrickleIce(): boolean {
82
+ return Boolean(this._currentConfig?.useTrickleIce);
83
+ }
84
+
78
85
  /**
79
86
  * Connect using credential authentication
80
87
  */
@@ -210,6 +217,12 @@ export class SessionManager {
210
217
  const storedPassword = await AsyncStorage.getItem('@telnyx_password');
211
218
  const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
212
219
  const storedPushToken = await AsyncStorage.getItem('@push_token');
220
+ const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
221
+ const storedMissedCallNotifications = await AsyncStorage.getItem(
222
+ MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
223
+ );
224
+ const useTrickleIce = storedUseTrickleIce === 'true';
225
+ const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
213
226
 
214
227
  // Check if we have credential-based authentication data
215
228
  if (storedUsername && storedPassword) {
@@ -217,6 +230,8 @@ export class SessionManager {
217
230
  const { createCredentialConfig } = require('../../models/config');
218
231
  this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
219
232
  pushNotificationDeviceToken: storedPushToken,
233
+ useTrickleIce,
234
+ enableMissedCallNotifications,
220
235
  });
221
236
  }
222
237
  // Check if we have token-based authentication data
@@ -225,6 +240,8 @@ export class SessionManager {
225
240
  const { createTokenConfig } = require('../../models/config');
226
241
  this._currentConfig = createTokenConfig(storedCredentialToken, {
227
242
  pushNotificationDeviceToken: storedPushToken,
243
+ useTrickleIce,
244
+ enableMissedCallNotifications,
228
245
  });
229
246
  }
230
247
 
@@ -353,6 +370,8 @@ export class SessionManager {
353
370
  logLevel: this._currentConfig.debug ? 'debug' : 'warn',
354
371
  debug: this._currentConfig.debug ?? false,
355
372
  pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
373
+ enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
374
+ useTrickleIce: this._currentConfig.useTrickleIce,
356
375
  enableCallReports: this._currentConfig.enableCallReports,
357
376
  callReportInterval: this._currentConfig.callReportInterval,
358
377
  callReportLogLevel: this._currentConfig.callReportLogLevel,
@@ -371,6 +390,8 @@ export class SessionManager {
371
390
  logLevel: this._currentConfig.debug ? 'debug' : 'warn',
372
391
  debug: this._currentConfig.debug ?? false,
373
392
  pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
393
+ enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
394
+ useTrickleIce: this._currentConfig.useTrickleIce,
374
395
  enableCallReports: this._currentConfig.enableCallReports,
375
396
  callReportInterval: this._currentConfig.callReportInterval,
376
397
  callReportLogLevel: this._currentConfig.callReportLogLevel,
@@ -38,6 +38,10 @@ export interface VoicePnBridgeInterface {
38
38
  clearPendingVoipPush(): Promise<boolean>;
39
39
  getPendingVoipAction(): Promise<string | null>;
40
40
  clearPendingVoipAction(): Promise<boolean>;
41
+ setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
42
+ getMissedCallNotificationsEnabled(): Promise<boolean>;
43
+ setSpeakerEnabled(enabled: boolean): Promise<boolean>;
44
+ isSpeakerEnabled(): Promise<boolean>;
41
45
  }
42
46
 
43
47
  const NativeBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
@@ -244,6 +248,66 @@ export class VoicePnBridge {
244
248
  }
245
249
  }
246
250
 
251
+ /**
252
+ * Enable or disable local missed call notifications on iOS.
253
+ * Missed-call VoIP pushes are still handled through CallKit either way.
254
+ */
255
+ static async setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean> {
256
+ try {
257
+ return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
258
+ } catch (error) {
259
+ console.error('VoicePnBridge: Error setting missed call notifications:', error);
260
+ return false;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Returns true by default, matching the sample-app behavior for missed calls.
266
+ */
267
+ static async getMissedCallNotificationsEnabled(): Promise<boolean> {
268
+ try {
269
+ return await NativeBridge.getMissedCallNotificationsEnabled();
270
+ } catch (error) {
271
+ console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
272
+ return true;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Route call audio through the loudspeaker when enabled, or back to the
278
+ * platform default voice route when disabled.
279
+ */
280
+ static async setSpeakerEnabled(enabled: boolean): Promise<boolean> {
281
+ try {
282
+ return await NativeBridge.setSpeakerEnabled(enabled);
283
+ } catch (error) {
284
+ console.error('VoicePnBridge: Error setting speaker route:', error);
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Returns whether the current audio route is using the loudspeaker.
291
+ */
292
+ static async isSpeakerEnabled(): Promise<boolean> {
293
+ try {
294
+ return await NativeBridge.isSpeakerEnabled();
295
+ } catch (error) {
296
+ console.error('VoicePnBridge: Error getting speaker route:', error);
297
+ throw error;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Toggle between loudspeaker and the platform default voice route.
303
+ */
304
+ static async toggleSpeaker(): Promise<boolean> {
305
+ // This is intentionally read-then-write because route changes can also come from
306
+ // the OS, Bluetooth, wired accessories, or the active call session.
307
+ const enabled = await VoicePnBridge.isSpeakerEnabled();
308
+ return VoicePnBridge.setSpeakerEnabled(!enabled);
309
+ }
310
+
247
311
  /**
248
312
  * Android → React Native: Listen for immediate call action events from notification buttons
249
313
  * Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
@@ -7,6 +7,10 @@ export interface CredentialConfig {
7
7
  sipPassword: string;
8
8
  debug?: boolean;
9
9
  pushNotificationDeviceToken?: string;
10
+ /** Enable native missed call push notifications. Default: false */
11
+ enableMissedCallNotifications?: boolean;
12
+ /** Enable Trickle ICE. Default: false */
13
+ useTrickleIce?: boolean;
10
14
  /** Enable automatic call quality reporting. Default: true */
11
15
  enableCallReports?: boolean;
12
16
  /** Stats collection interval in seconds. Default: 5 */
@@ -25,6 +29,10 @@ export interface TokenConfig {
25
29
  token: string;
26
30
  debug?: boolean;
27
31
  pushNotificationDeviceToken?: string;
32
+ /** Enable native missed call push notifications. Default: false */
33
+ enableMissedCallNotifications?: boolean;
34
+ /** Enable Trickle ICE. Default: false */
35
+ useTrickleIce?: boolean;
28
36
  /** Enable automatic call quality reporting. Default: true */
29
37
  enableCallReports?: boolean;
30
38
  /** Stats collection interval in seconds. Default: 5 */
@@ -7,6 +7,9 @@ import { SessionManager } from './internal/session/session-manager';
7
7
  import { CallStateController } from './internal/calls/call-state-controller';
8
8
  import { VoicePnBridge } from './internal/voice-pn-bridge';
9
9
 
10
+ const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
11
+ const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
12
+
10
13
  /**
11
14
  * Configuration options for TelnyxVoipClient
12
15
  */
@@ -16,6 +19,9 @@ export interface TelnyxVoipClientOptions {
16
19
 
17
20
  /** Enable debug logging */
18
21
  debug?: boolean;
22
+
23
+ /** Enable Trickle ICE for calls created by this client */
24
+ useTrickleIce?: boolean;
19
25
  }
20
26
 
21
27
  /**
@@ -67,6 +73,7 @@ export class TelnyxVoipClient {
67
73
  this._options = {
68
74
  enableAppStateManagement: true,
69
75
  debug: false,
76
+ useTrickleIce: false,
70
77
  ...options,
71
78
  };
72
79
 
@@ -218,10 +225,15 @@ export class TelnyxVoipClient {
218
225
  console.log('TelnyxVoipClient: Logging in with credentials for user:', config.sipUser);
219
226
  }
220
227
 
228
+ const loginConfig = {
229
+ ...config,
230
+ useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
231
+ };
232
+
221
233
  // Store credentials for future reconnection
222
- await this._storeCredentials(config);
234
+ await this._storeCredentials(loginConfig);
223
235
 
224
- await this._sessionManager.connectWithCredential(config);
236
+ await this._sessionManager.connectWithCredential(loginConfig);
225
237
  }
226
238
 
227
239
  /**
@@ -245,10 +257,15 @@ export class TelnyxVoipClient {
245
257
  console.log('TelnyxVoipClient: Logging in with token');
246
258
  }
247
259
 
260
+ const loginConfig = {
261
+ ...config,
262
+ useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
263
+ };
264
+
248
265
  // Store token for future reconnection
249
- await this._storeToken(config);
266
+ await this._storeToken(loginConfig);
250
267
 
251
- await this._sessionManager.connectWithToken(config);
268
+ await this._sessionManager.connectWithToken(loginConfig);
252
269
  }
253
270
 
254
271
  /**
@@ -292,6 +309,13 @@ export class TelnyxVoipClient {
292
309
  const storedPassword = await AsyncStorage.getItem('@telnyx_password');
293
310
  const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
294
311
  const storedPushToken = await AsyncStorage.getItem('@push_token');
312
+ const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
313
+ const storedMissedCallNotifications = await AsyncStorage.getItem(
314
+ MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
315
+ );
316
+ const useTrickleIce =
317
+ storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
318
+ const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
295
319
 
296
320
  // Check if we have credential-based authentication data
297
321
  if (storedUsername && storedPassword) {
@@ -299,6 +323,8 @@ export class TelnyxVoipClient {
299
323
  const { createCredentialConfig } = require('./models/config');
300
324
  const config = createCredentialConfig(storedUsername, storedPassword, {
301
325
  pushNotificationDeviceToken: storedPushToken,
326
+ useTrickleIce,
327
+ enableMissedCallNotifications,
302
328
  });
303
329
 
304
330
  if (this._options.debug) {
@@ -318,6 +344,8 @@ export class TelnyxVoipClient {
318
344
  const { createTokenConfig } = require('./models/config');
319
345
  const config = createTokenConfig(storedCredentialToken, {
320
346
  pushNotificationDeviceToken: storedPushToken,
347
+ useTrickleIce,
348
+ enableMissedCallNotifications,
321
349
  });
322
350
 
323
351
  if (this._options.debug) {
@@ -528,6 +556,14 @@ export class TelnyxVoipClient {
528
556
 
529
557
  await AsyncStorage.setItem('@telnyx_username', config.sipUser);
530
558
  await AsyncStorage.setItem('@telnyx_password', config.sipPassword);
559
+ await AsyncStorage.setItem(
560
+ USE_TRICKLE_ICE_STORAGE_KEY,
561
+ String(config.useTrickleIce ?? false)
562
+ );
563
+ await AsyncStorage.setItem(
564
+ MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
565
+ String(config.enableMissedCallNotifications ?? false)
566
+ );
531
567
 
532
568
  if (config.pushNotificationDeviceToken) {
533
569
  await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
@@ -555,6 +591,14 @@ export class TelnyxVoipClient {
555
591
  const AsyncStorage = require('@react-native-async-storage/async-storage').default;
556
592
 
557
593
  await AsyncStorage.setItem('@credential_token', config.token);
594
+ await AsyncStorage.setItem(
595
+ USE_TRICKLE_ICE_STORAGE_KEY,
596
+ String(config.useTrickleIce ?? false)
597
+ );
598
+ await AsyncStorage.setItem(
599
+ MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
600
+ String(config.enableMissedCallNotifications ?? false)
601
+ );
558
602
 
559
603
  if (config.pushNotificationDeviceToken) {
560
604
  await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
@@ -14,6 +14,14 @@ declare module '@telnyx/react-native-voice-sdk' {
14
14
  destinationNumber?: string;
15
15
  audio?: boolean;
16
16
  video?: boolean;
17
+ peerConnectionOptions?: {
18
+ prefetchIceCandidates?: boolean;
19
+ useTrickleIce?: boolean;
20
+ iceServers?: any[];
21
+ iceTransportPolicy?: string;
22
+ bundlePolicy?: string;
23
+ rtcpMuxPolicy?: string;
24
+ };
17
25
  }
18
26
 
19
27
  export interface ClientOptions {
@@ -26,6 +34,8 @@ declare module '@telnyx/react-native-voice-sdk' {
26
34
  debug?: boolean;
27
35
  logLevel?: string;
28
36
  pushNotificationDeviceToken?: string;
37
+ enableMissedCallNotifications?: boolean;
38
+ useTrickleIce?: boolean;
29
39
  enableCallReports?: boolean;
30
40
  callReportInterval?: number;
31
41
  callReportLogLevel?: string;