@telnyx/react-voice-commons-sdk 0.4.3 → 1.1.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 (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -7
  3. package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
  4. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
  5. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
  6. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
  7. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
  8. package/ios/CallKitBridge.m +20 -1
  9. package/ios/CallKitBridge.swift +935 -60
  10. package/ios/README.md +2 -2
  11. package/ios/VoicePnBridge.m +21 -1
  12. package/ios/VoicePnBridge.swift +62 -1
  13. package/lib/callkit/callkit-coordinator.d.ts +39 -3
  14. package/lib/callkit/callkit-coordinator.js +561 -148
  15. package/lib/callkit/callkit.d.ts +7 -0
  16. package/lib/callkit/callkit.js +83 -11
  17. package/lib/hooks/useAppStateHandler.js +6 -2
  18. package/lib/index.d.ts +1 -0
  19. package/lib/index.js +9 -0
  20. package/lib/internal/CallKitHandler.d.ts +3 -0
  21. package/lib/internal/CallKitHandler.js +52 -46
  22. package/lib/internal/calls/call-state-controller.d.ts +39 -1
  23. package/lib/internal/calls/call-state-controller.js +152 -64
  24. package/lib/internal/session/session-manager.d.ts +16 -1
  25. package/lib/internal/session/session-manager.js +229 -58
  26. package/lib/internal/voice-pn-bridge.d.ts +44 -0
  27. package/lib/internal/voice-pn-bridge.js +94 -0
  28. package/lib/models/call.js +35 -0
  29. package/lib/models/config.d.ts +18 -0
  30. package/lib/models/config.js +2 -0
  31. package/lib/telnyx-voice-app.js +85 -43
  32. package/lib/telnyx-voip-client.d.ts +48 -5
  33. package/lib/telnyx-voip-client.js +276 -22
  34. package/package.json +7 -5
  35. package/src/callkit/callkit-coordinator.ts +642 -160
  36. package/src/callkit/callkit.ts +110 -12
  37. package/src/hooks/useAppStateHandler.ts +7 -2
  38. package/src/index.ts +3 -0
  39. package/src/internal/CallKitHandler.tsx +60 -55
  40. package/src/internal/calls/call-state-controller.ts +173 -74
  41. package/src/internal/session/session-manager.ts +277 -61
  42. package/src/internal/voice-pn-bridge.ts +111 -0
  43. package/src/models/call.ts +34 -0
  44. package/src/models/config.ts +18 -0
  45. package/src/telnyx-voice-app.tsx +96 -45
  46. package/src/telnyx-voip-client.ts +260 -24
  47. package/src/types/telnyx-sdk.d.ts +12 -1
@@ -59,6 +59,9 @@ const TelnyxSDK = __importStar(require('@telnyx/react-native-voice-sdk'));
59
59
  const pkg = __importStar(require('../../../package.json'));
60
60
  const connection_state_1 = require('../../models/connection-state');
61
61
  const config_1 = require('../../models/config');
62
+ const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
63
+ const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
64
+ const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
62
65
  /**
63
66
  * Manages the connection lifecycle to the Telnyx platform.
64
67
  *
@@ -71,6 +74,8 @@ class SessionManager {
71
74
  connection_state_1.TelnyxConnectionState.DISCONNECTED
72
75
  );
73
76
  this._disposed = false;
77
+ this._disposing = false;
78
+ this._connectionGeneration = 0;
74
79
  this._sessionId = this._generateSessionId();
75
80
  }
76
81
  /**
@@ -110,25 +115,26 @@ class SessionManager {
110
115
  get telnyxClient() {
111
116
  return this._telnyxClient;
112
117
  }
118
+ get useTrickleIce() {
119
+ return Boolean(this._currentConfig?.useTrickleIce);
120
+ }
113
121
  /**
114
122
  * Connect using credential authentication
115
123
  */
116
124
  async connectWithCredential(config) {
117
- if (this._disposed) {
118
- throw new Error('SessionManager has been disposed');
119
- }
125
+ this._assertCanStartConnection();
120
126
  this._currentConfig = config;
121
127
  await this._connect();
128
+ this._assertCanStartConnection();
122
129
  }
123
130
  /**
124
131
  * Connect using token authentication
125
132
  */
126
133
  async connectWithToken(config) {
127
- if (this._disposed) {
128
- throw new Error('SessionManager has been disposed');
129
- }
134
+ this._assertCanStartConnection();
130
135
  this._currentConfig = config;
131
136
  await this._connect();
137
+ this._assertCanStartConnection();
132
138
  }
133
139
  /**
134
140
  * Disconnect from the Telnyx platform.
@@ -144,6 +150,7 @@ class SessionManager {
144
150
  if (this._disposed) {
145
151
  return;
146
152
  }
153
+ this._connectionGeneration += 1;
147
154
  this._currentConfig = undefined;
148
155
  this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
149
156
  if (this._onDisconnect) {
@@ -154,11 +161,7 @@ class SessionManager {
154
161
  }
155
162
  }
156
163
  if (this._telnyxClient) {
157
- try {
158
- await this._telnyxClient.disconnect();
159
- } catch (error) {
160
- console.error('Error during disconnect:', error);
161
- }
164
+ await this._disconnectAndForgetClient(this._telnyxClient, 'Error during disconnect:');
162
165
  }
163
166
  }
164
167
  /**
@@ -168,6 +171,7 @@ class SessionManager {
168
171
  */
169
172
  disablePushNotifications() {
170
173
  if (
174
+ !this._isTeardownActive() &&
171
175
  this._telnyxClient &&
172
176
  this.currentState === connection_state_1.TelnyxConnectionState.CONNECTED
173
177
  ) {
@@ -181,7 +185,7 @@ class SessionManager {
181
185
  * Handle push notification with stored config
182
186
  */
183
187
  handlePushNotificationWithConfig(pushMetaData, config) {
184
- if (this._disposed) {
188
+ if (this._isTeardownActive()) {
185
189
  return;
186
190
  }
187
191
  this._currentConfig = config;
@@ -193,7 +197,7 @@ class SessionManager {
193
197
  * Handle push notification (async version)
194
198
  */
195
199
  async handlePushNotification(payload) {
196
- if (this._disposed) {
200
+ if (this._isTeardownActive()) {
197
201
  return;
198
202
  }
199
203
  console.log(
@@ -202,19 +206,33 @@ class SessionManager {
202
206
  );
203
207
  // Store the push notification payload for when the client is created
204
208
  this._pendingPushPayload = payload;
205
- // Each push always rebuilds the TelnyxRTC client. The voice_sdk_id is
206
- // baked into the WebSocket URL at socket-open time and can't be mutated
207
- // mid-flight; reusing a connection from a previous push means the
208
- // gateway routes THIS push's INVITE to a different (correctly-stamped)
209
- // client and we sit on the wrong socket forever.
210
- if (this._telnyxClient) {
211
- try {
212
- await this._telnyxClient.disconnect();
213
- } catch (err) {
214
- console.warn('SessionManager: disconnect of prior client threw:', err);
209
+ // A second-call push must stay on the client that owns the active/held
210
+ // call. Tearing it down destroys the first signaling session and clears
211
+ // the call collection before CallKit can complete Hold & Accept.
212
+ if (this._telnyxClient && this._hasActiveOrHeldCall(this._telnyxClient)) {
213
+ const actualPayload = this._extractPushPayload(payload);
214
+ const processVoIPNotification = this._telnyxClient.processVoIPNotification;
215
+ if (typeof processVoIPNotification !== 'function') {
216
+ throw new Error('TelnyxRTC client cannot process an active-call VoIP notification');
215
217
  }
218
+ console.log(
219
+ 'SessionManager: Preserving active client while processing second-call push notification'
220
+ );
221
+ processVoIPNotification.call(this._telnyxClient, actualPayload);
222
+ this._pendingPushPayload = null;
223
+ return;
224
+ }
225
+ // The cold/no-active-call path still rebuilds the client so the socket is
226
+ // stamped with the push voice_sdk_id before connecting.
227
+ if (this._telnyxClient) {
228
+ await this._disconnectAndForgetClient(
229
+ this._telnyxClient,
230
+ 'SessionManager: disconnect of prior client threw:'
231
+ );
232
+ }
233
+ if (this._isTeardownActive()) {
234
+ return;
216
235
  }
217
- this._telnyxClient = undefined;
218
236
  if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
219
237
  this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
220
238
  }
@@ -231,12 +249,26 @@ class SessionManager {
231
249
  const storedPassword = await AsyncStorage.getItem('@telnyx_password');
232
250
  const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
233
251
  const storedPushToken = await AsyncStorage.getItem('@push_token');
252
+ const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
253
+ const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
254
+ const storedMissedCallNotifications = await AsyncStorage.getItem(
255
+ MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
256
+ );
257
+ const useTrickleIce = storedUseTrickleIce === 'true';
258
+ const pushWhenActive = storedPushWhenActive === 'true';
259
+ const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
260
+ if (this._isTeardownActive()) {
261
+ return;
262
+ }
234
263
  // Check if we have credential-based authentication data
235
264
  if (storedUsername && storedPassword) {
236
265
  console.log('SessionManager: RELEASE DEBUG - Found stored credentials, creating config');
237
266
  const { createCredentialConfig } = require('../../models/config');
238
267
  this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
239
268
  pushNotificationDeviceToken: storedPushToken,
269
+ useTrickleIce,
270
+ pushWhenActive,
271
+ enableMissedCallNotifications,
240
272
  });
241
273
  }
242
274
  // Check if we have token-based authentication data
@@ -245,6 +277,9 @@ class SessionManager {
245
277
  const { createTokenConfig } = require('../../models/config');
246
278
  this._currentConfig = createTokenConfig(storedCredentialToken, {
247
279
  pushNotificationDeviceToken: storedPushToken,
280
+ useTrickleIce,
281
+ pushWhenActive,
282
+ enableMissedCallNotifications,
248
283
  });
249
284
  }
250
285
  if (this._currentConfig) {
@@ -258,6 +293,9 @@ class SessionManager {
258
293
  console.warn('SessionManager: Failed to load stored config for push notification:', error);
259
294
  }
260
295
  }
296
+ if (this._isTeardownActive()) {
297
+ return;
298
+ }
261
299
  // If we already have a client, process the push notification immediately
262
300
  if (this._telnyxClient) {
263
301
  console.log(
@@ -302,10 +340,16 @@ class SessionManager {
302
340
  );
303
341
  try {
304
342
  await this._connect();
343
+ if (this._isTeardownActive()) {
344
+ return;
345
+ }
305
346
  console.log(
306
347
  'SessionManager: RELEASE DEBUG - Successfully connected after push notification trigger'
307
348
  );
308
349
  } catch (error) {
350
+ if (this._isTeardownActive()) {
351
+ return;
352
+ }
309
353
  console.error(
310
354
  'SessionManager: Failed to connect after push notification trigger:',
311
355
  error
@@ -325,73 +369,138 @@ class SessionManager {
325
369
  }
326
370
  console.log('SessionManager: RELEASE DEBUG - Push notification handling complete');
327
371
  }
372
+ _hasActiveOrHeldCall(client) {
373
+ const calls =
374
+ typeof client.getActiveCalls === 'function'
375
+ ? client.getActiveCalls()
376
+ : Array.from(client.calls?.values?.() || []);
377
+ return calls.some((call) => call?.state === 'active' || call?.state === 'held');
378
+ }
328
379
  /**
329
380
  * Dispose of the session manager and clean up resources
330
381
  */
331
- dispose() {
382
+ async dispose() {
332
383
  if (this._disposed) {
333
384
  return;
334
385
  }
335
- this._disposed = true;
336
- this.disconnect();
337
- this._connectionState.complete();
386
+ if (!this._disposePromise) {
387
+ this._disposePromise = this._dispose();
388
+ }
389
+ await this._disposePromise;
338
390
  }
339
391
  /**
340
392
  * Internal method to establish connection with or without push notification handling
341
393
  */
342
394
  async _connect() {
343
- if (!this._currentConfig) {
395
+ const previousConnect = this._connectPromise;
396
+ if (previousConnect) {
397
+ try {
398
+ await previousConnect;
399
+ } catch (error) {
400
+ if (this._isTeardownActive()) {
401
+ throw error;
402
+ }
403
+ }
404
+ }
405
+ this._assertCanStartConnection();
406
+ const config = this._currentConfig;
407
+ if (!config) {
344
408
  throw new Error('No configuration provided');
345
409
  }
410
+ const connectionGeneration = (this._connectionGeneration += 1);
411
+ const connectPromise = this._runConnect(config, connectionGeneration);
412
+ this._connectPromise = connectPromise;
413
+ try {
414
+ await connectPromise;
415
+ } finally {
416
+ if (this._connectPromise === connectPromise) {
417
+ this._connectPromise = undefined;
418
+ }
419
+ }
420
+ }
421
+ async _dispose() {
422
+ this._disposing = true;
423
+ this._connectionGeneration += 1;
424
+ const inFlightConnect = this._connectPromise;
425
+ await this.disconnect();
426
+ if (inFlightConnect) {
427
+ try {
428
+ await inFlightConnect;
429
+ } catch {
430
+ // A connect canceled by disposal is expected; the cleanup path runs in _runConnect.
431
+ }
432
+ }
433
+ const client = this._telnyxClient;
434
+ if (client) {
435
+ await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
436
+ }
437
+ this._currentConfig = undefined;
438
+ this._pendingPushPayload = null;
439
+ this._disposed = true;
440
+ this._disposing = false;
441
+ this._connectionState.complete();
442
+ }
443
+ async _runConnect(config, connectionGeneration) {
444
+ this._throwIfConnectCanceled(connectionGeneration);
346
445
  this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTING);
446
+ let client;
447
+ let canceledConnectCleanupCompleted = false;
347
448
  try {
348
449
  // Clean up existing client
349
450
  if (this._telnyxClient) {
350
- await this._telnyxClient.disconnect();
451
+ await this._disconnectClient(this._telnyxClient, 'Error during disconnect:');
351
452
  }
453
+ this._throwIfConnectCanceled(connectionGeneration);
352
454
  // Create new client instance with authentication options
353
455
  let clientOptions;
354
- if ((0, config_1.isCredentialConfig)(this._currentConfig)) {
456
+ if ((0, config_1.isCredentialConfig)(config)) {
355
457
  clientOptions = {
356
- login: this._currentConfig.sipUser,
357
- password: this._currentConfig.sipPassword,
358
- logLevel: this._currentConfig.debug ? 'debug' : 'warn',
359
- debug: this._currentConfig.debug ?? false,
360
- pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
361
- enableCallReports: this._currentConfig.enableCallReports,
362
- callReportInterval: this._currentConfig.callReportInterval,
363
- callReportLogLevel: this._currentConfig.callReportLogLevel,
364
- callReportMaxLogEntries: this._currentConfig.callReportMaxLogEntries,
458
+ login: config.sipUser,
459
+ password: config.sipPassword,
460
+ logLevel: config.debug ? 'debug' : 'warn',
461
+ debug: config.debug ?? false,
462
+ pushNotificationDeviceToken: config.pushNotificationDeviceToken,
463
+ pushWhenActive: config.pushWhenActive,
464
+ enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
465
+ useTrickleIce: config.useTrickleIce,
466
+ enableCallReports: config.enableCallReports,
467
+ callReportInterval: config.callReportInterval,
468
+ callReportLogLevel: config.callReportLogLevel,
469
+ callReportMaxLogEntries: config.callReportMaxLogEntries,
365
470
  sdkVersion: pkg.version,
366
471
  };
367
472
  console.log(
368
473
  '🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
369
474
  clientOptions.logLevel,
370
475
  'pushToken:',
371
- !!this._currentConfig.pushNotificationDeviceToken
476
+ !!config.pushNotificationDeviceToken
372
477
  );
373
- } else if ((0, config_1.isTokenConfig)(this._currentConfig)) {
478
+ } else if ((0, config_1.isTokenConfig)(config)) {
374
479
  clientOptions = {
375
- login_token: this._currentConfig.token,
376
- logLevel: this._currentConfig.debug ? 'debug' : 'warn',
377
- debug: this._currentConfig.debug ?? false,
378
- pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
379
- enableCallReports: this._currentConfig.enableCallReports,
380
- callReportInterval: this._currentConfig.callReportInterval,
381
- callReportLogLevel: this._currentConfig.callReportLogLevel,
382
- callReportMaxLogEntries: this._currentConfig.callReportMaxLogEntries,
480
+ login_token: config.token,
481
+ logLevel: config.debug ? 'debug' : 'warn',
482
+ debug: config.debug ?? false,
483
+ pushNotificationDeviceToken: config.pushNotificationDeviceToken,
484
+ pushWhenActive: config.pushWhenActive,
485
+ enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
486
+ useTrickleIce: config.useTrickleIce,
487
+ enableCallReports: config.enableCallReports,
488
+ callReportInterval: config.callReportInterval,
489
+ callReportLogLevel: config.callReportLogLevel,
490
+ callReportMaxLogEntries: config.callReportMaxLogEntries,
383
491
  sdkVersion: pkg.version,
384
492
  };
385
493
  console.log(
386
494
  '🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
387
495
  clientOptions.logLevel,
388
496
  'pushToken:',
389
- !!this._currentConfig.pushNotificationDeviceToken
497
+ !!config.pushNotificationDeviceToken
390
498
  );
391
499
  } else {
392
500
  throw new Error('Invalid configuration type');
393
501
  }
394
- this._telnyxClient = new TelnyxSDK.TelnyxRTC(clientOptions);
502
+ client = new TelnyxSDK.TelnyxRTC(clientOptions);
503
+ this._telnyxClient = client;
395
504
  // CRITICAL: Process any pending push notification payload BEFORE connecting
396
505
  // This ensures voice_sdk_id and other payload variables are set before connect() is called
397
506
  const pendingPushPayload = this._pendingPushPayload;
@@ -418,7 +527,7 @@ class SessionManager {
418
527
  // Clear the pending payload
419
528
  this._pendingPushPayload = null;
420
529
  }
421
- this._setupClientListeners();
530
+ this._setupClientListeners(client);
422
531
  // Set up CallStateController listeners immediately after client creation
423
532
  // This ensures they're ready before any incoming call events are emitted
424
533
  console.log(
@@ -432,15 +541,34 @@ class SessionManager {
432
541
  } else {
433
542
  console.log('🔧 SessionManager: No _onClientReady callback found');
434
543
  }
544
+ this._throwIfConnectCanceled(connectionGeneration);
435
545
  // Connect to the platform AFTER processing push notification
436
546
  console.log(
437
547
  'SessionManager: RELEASE DEBUG - About to call connect() after processing push notification'
438
548
  );
439
- await this._telnyxClient.connect();
549
+ await client.connect();
550
+ if (this._isConnectCanceled(connectionGeneration)) {
551
+ await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
552
+ canceledConnectCleanupCompleted = true;
553
+ throw this._createConnectCanceledError();
554
+ }
440
555
  // Notify that client is ready for event listeners
441
556
  console.log('🔧 SessionManager: Client connected successfully');
442
557
  } catch (error) {
558
+ if (this._isConnectCanceled(connectionGeneration)) {
559
+ if (client && !canceledConnectCleanupCompleted) {
560
+ await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
561
+ }
562
+ throw this._createConnectCanceledError();
563
+ }
443
564
  console.error('Connection failed:', error);
565
+ if (client) {
566
+ try {
567
+ await this._disconnectAndForgetClient(client, 'Error during failed connect cleanup:');
568
+ } catch (cleanupError) {
569
+ console.error('Error during failed connect cleanup:', cleanupError);
570
+ }
571
+ }
444
572
  this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
445
573
  throw error;
446
574
  }
@@ -448,11 +576,14 @@ class SessionManager {
448
576
  /**
449
577
  * Set up event listeners for the Telnyx client
450
578
  */
451
- _setupClientListeners() {
452
- if (!this._telnyxClient) {
579
+ _setupClientListeners(client) {
580
+ if (!client) {
453
581
  return;
454
582
  }
455
- this._telnyxClient.on('telnyx.client.ready', () => {
583
+ client.on('telnyx.client.ready', () => {
584
+ if (this._isTeardownActive() || this._telnyxClient !== client) {
585
+ return;
586
+ }
456
587
  console.log('Telnyx client ready');
457
588
  this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTED);
458
589
  // Ensure CallStateController listeners are set up when client becomes ready
@@ -470,7 +601,10 @@ class SessionManager {
470
601
  console.log('🔧 SessionManager: No _onClientReady callback found in client ready event');
471
602
  }
472
603
  });
473
- this._telnyxClient.on('telnyx.client.error', (error) => {
604
+ client.on('telnyx.client.error', (error) => {
605
+ if (this._isTeardownActive() || this._telnyxClient !== client) {
606
+ return;
607
+ }
474
608
  console.error('Telnyx client error:', error);
475
609
  this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
476
610
  });
@@ -526,5 +660,42 @@ class SessionManager {
526
660
  _generateSessionId() {
527
661
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
528
662
  }
663
+ _assertCanStartConnection() {
664
+ if (this._isTeardownActive()) {
665
+ throw new Error('SessionManager has been disposed');
666
+ }
667
+ }
668
+ _isTeardownActive() {
669
+ return this._disposed || this._disposing;
670
+ }
671
+ _isConnectCanceled(connectionGeneration) {
672
+ return this._isTeardownActive() || this._connectionGeneration !== connectionGeneration;
673
+ }
674
+ _throwIfConnectCanceled(connectionGeneration) {
675
+ if (this._isConnectCanceled(connectionGeneration)) {
676
+ throw this._createConnectCanceledError();
677
+ }
678
+ }
679
+ _createConnectCanceledError() {
680
+ return new Error(
681
+ this._isTeardownActive()
682
+ ? 'SessionManager has been disposed'
683
+ : 'SessionManager connection has been canceled'
684
+ );
685
+ }
686
+ async _disconnectClient(client, errorMessage) {
687
+ try {
688
+ await client.disconnect();
689
+ } catch (error) {
690
+ console.error(errorMessage, error);
691
+ throw error;
692
+ }
693
+ }
694
+ async _disconnectAndForgetClient(client, errorMessage) {
695
+ await this._disconnectClient(client, errorMessage);
696
+ if (this._telnyxClient === client) {
697
+ this._telnyxClient = undefined;
698
+ }
699
+ }
529
700
  }
530
701
  exports.SessionManager = SessionManager;
@@ -25,11 +25,18 @@ export interface VoicePnBridgeInterface {
25
25
  ): Promise<boolean>;
26
26
  hideOngoingCallNotification(): Promise<boolean>;
27
27
  hideIncomingCallNotification(): Promise<boolean>;
28
+ setIncomingCallRingtone(resourceName: string | null): Promise<boolean>;
29
+ getPendingCallKitAnswer(): Promise<string | null>;
30
+ clearPendingCallKitAnswer(): Promise<boolean>;
28
31
  getVoipToken(): Promise<string | null>;
29
32
  getPendingVoipPush(): Promise<string | null>;
30
33
  clearPendingVoipPush(): Promise<boolean>;
31
34
  getPendingVoipAction(): Promise<string | null>;
32
35
  clearPendingVoipAction(): Promise<boolean>;
36
+ setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
37
+ getMissedCallNotificationsEnabled(): Promise<boolean>;
38
+ setSpeakerEnabled(enabled: boolean): Promise<boolean>;
39
+ isSpeakerEnabled(): Promise<boolean>;
33
40
  }
34
41
  declare const NativeBridge: VoicePnBridgeInterface;
35
42
  /**
@@ -87,6 +94,21 @@ export declare class VoicePnBridge {
87
94
  * Useful for dismissing notifications when call is answered/rejected in app
88
95
  */
89
96
  static hideIncomingCallNotification(): Promise<boolean>;
97
+ /**
98
+ * Configure Android's incoming-call ringtone using an app resource in `res/raw`.
99
+ * Pass no value to use the device's default phone ringtone.
100
+ */
101
+ static setIncomingCallRingtone(resourceName?: string): Promise<boolean>;
102
+ /**
103
+ * Get pending CallKit answer UUID from native storage (iOS only).
104
+ * When the user answers a CallKit call before JS listeners are ready,
105
+ * the native side persists the answer UUID in UserDefaults so JS can detect it.
106
+ */
107
+ static getPendingCallKitAnswer(): Promise<string | null>;
108
+ /**
109
+ * Clear pending CallKit answer from native storage (iOS only)
110
+ */
111
+ static clearPendingCallKitAnswer(): Promise<boolean>;
90
112
  /**
91
113
  * Get VoIP token from native storage
92
114
  */
@@ -107,6 +129,28 @@ export declare class VoicePnBridge {
107
129
  * Clear pending VoIP action from native storage (iOS only)
108
130
  */
109
131
  static clearPendingVoipAction(): Promise<boolean>;
132
+ /**
133
+ * Enable or disable local missed call notifications on iOS.
134
+ * Missed-call VoIP pushes are still handled through CallKit either way.
135
+ */
136
+ static setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
137
+ /**
138
+ * Returns true by default, matching the sample-app behavior for missed calls.
139
+ */
140
+ static getMissedCallNotificationsEnabled(): Promise<boolean>;
141
+ /**
142
+ * Route call audio through the loudspeaker when enabled, or back to the
143
+ * platform default voice route when disabled.
144
+ */
145
+ static setSpeakerEnabled(enabled: boolean): Promise<boolean>;
146
+ /**
147
+ * Returns whether the current audio route is using the loudspeaker.
148
+ */
149
+ static isSpeakerEnabled(): Promise<boolean>;
150
+ /**
151
+ * Toggle between loudspeaker and the platform default voice route.
152
+ */
153
+ static toggleSpeaker(): Promise<boolean>;
110
154
  /**
111
155
  * Android → React Native: Listen for immediate call action events from notification buttons
112
156
  * Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
@@ -124,6 +124,45 @@ class VoicePnBridge {
124
124
  return false;
125
125
  }
126
126
  }
127
+ /**
128
+ * Configure Android's incoming-call ringtone using an app resource in `res/raw`.
129
+ * Pass no value to use the device's default phone ringtone.
130
+ */
131
+ static async setIncomingCallRingtone(resourceName) {
132
+ if (react_native_1.Platform.OS !== 'android') return true;
133
+ try {
134
+ return await NativeBridge.setIncomingCallRingtone(resourceName?.trim() || null);
135
+ } catch (error) {
136
+ console.error('VoicePnBridge: Error setting incoming call ringtone:', error);
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Get pending CallKit answer UUID from native storage (iOS only).
142
+ * When the user answers a CallKit call before JS listeners are ready,
143
+ * the native side persists the answer UUID in UserDefaults so JS can detect it.
144
+ */
145
+ static async getPendingCallKitAnswer() {
146
+ if (react_native_1.Platform.OS !== 'ios') return null;
147
+ try {
148
+ return await NativeBridge.getPendingCallKitAnswer();
149
+ } catch (error) {
150
+ console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Clear pending CallKit answer from native storage (iOS only)
156
+ */
157
+ static async clearPendingCallKitAnswer() {
158
+ if (react_native_1.Platform.OS !== 'ios') return true;
159
+ try {
160
+ return await NativeBridge.clearPendingCallKitAnswer();
161
+ } catch (error) {
162
+ console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
163
+ return false;
164
+ }
165
+ }
127
166
  /**
128
167
  * Get VoIP token from native storage
129
168
  */
@@ -184,6 +223,61 @@ class VoicePnBridge {
184
223
  return false;
185
224
  }
186
225
  }
226
+ /**
227
+ * Enable or disable local missed call notifications on iOS.
228
+ * Missed-call VoIP pushes are still handled through CallKit either way.
229
+ */
230
+ static async setMissedCallNotificationsEnabled(enabled) {
231
+ try {
232
+ return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
233
+ } catch (error) {
234
+ console.error('VoicePnBridge: Error setting missed call notifications:', error);
235
+ return false;
236
+ }
237
+ }
238
+ /**
239
+ * Returns true by default, matching the sample-app behavior for missed calls.
240
+ */
241
+ static async getMissedCallNotificationsEnabled() {
242
+ try {
243
+ return await NativeBridge.getMissedCallNotificationsEnabled();
244
+ } catch (error) {
245
+ console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
246
+ return true;
247
+ }
248
+ }
249
+ /**
250
+ * Route call audio through the loudspeaker when enabled, or back to the
251
+ * platform default voice route when disabled.
252
+ */
253
+ static async setSpeakerEnabled(enabled) {
254
+ try {
255
+ return await NativeBridge.setSpeakerEnabled(enabled);
256
+ } catch (error) {
257
+ console.error('VoicePnBridge: Error setting speaker route:', error);
258
+ throw error;
259
+ }
260
+ }
261
+ /**
262
+ * Returns whether the current audio route is using the loudspeaker.
263
+ */
264
+ static async isSpeakerEnabled() {
265
+ try {
266
+ return await NativeBridge.isSpeakerEnabled();
267
+ } catch (error) {
268
+ console.error('VoicePnBridge: Error getting speaker route:', error);
269
+ throw error;
270
+ }
271
+ }
272
+ /**
273
+ * Toggle between loudspeaker and the platform default voice route.
274
+ */
275
+ static async toggleSpeaker() {
276
+ // This is intentionally read-then-write because route changes can also come from
277
+ // the OS, Bluetooth, wired accessories, or the active call session.
278
+ const enabled = await VoicePnBridge.isSpeakerEnabled();
279
+ return VoicePnBridge.setSpeakerEnabled(!enabled);
280
+ }
187
281
  /**
188
282
  * Android → React Native: Listen for immediate call action events from notification buttons
189
283
  * Use this for active calls where immediate response is needed (e.g., ending ongoing calls)