@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
@@ -314,6 +314,18 @@ class Call {
314
314
  throw new Error(`Cannot hold call in state: ${this.currentState}`);
315
315
  }
316
316
  try {
317
+ if (react_native_1.Platform.OS === 'ios') {
318
+ const { callKitCoordinator } = await Promise.resolve().then(() =>
319
+ __importStar(require('../callkit/callkit-coordinator'))
320
+ );
321
+ if (callKitCoordinator.isAvailable()) {
322
+ const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, true);
323
+ if (!success) {
324
+ throw new Error('CallKit failed to hold call');
325
+ }
326
+ return;
327
+ }
328
+ }
317
329
  await this._telnyxCall.hold();
318
330
  } catch (error) {
319
331
  console.error('Failed to hold call:', error);
@@ -328,6 +340,18 @@ class Call {
328
340
  throw new Error(`Cannot resume call in state: ${this.currentState}`);
329
341
  }
330
342
  try {
343
+ if (react_native_1.Platform.OS === 'ios') {
344
+ const { callKitCoordinator } = await Promise.resolve().then(() =>
345
+ __importStar(require('../callkit/callkit-coordinator'))
346
+ );
347
+ if (callKitCoordinator.isAvailable()) {
348
+ const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, false);
349
+ if (!success) {
350
+ throw new Error('CallKit failed to resume call');
351
+ }
352
+ return;
353
+ }
354
+ }
331
355
  await this._telnyxCall.unhold();
332
356
  } catch (error) {
333
357
  console.error('Failed to resume call:', error);
@@ -422,6 +446,17 @@ class Call {
422
446
  this._telnyxCall.on('telnyx.call.state', (call, state) => {
423
447
  const telnyxState = this._mapToTelnyxCallState(state);
424
448
  this._callState.next(telnyxState);
449
+ // Keep the dedicated held observable derived from call state. Updating
450
+ // this only after the low-level SDK emits its successful transition
451
+ // preserves the previous value when hold/unhold signaling fails.
452
+ if (telnyxState === call_state_1.TelnyxCallState.HELD) {
453
+ this._isHeld.next(true);
454
+ } else if (
455
+ telnyxState === call_state_1.TelnyxCallState.ACTIVE ||
456
+ call_state_1.CallStateHelpers.isTerminated(telnyxState)
457
+ ) {
458
+ this._isHeld.next(false);
459
+ }
425
460
  // Start duration timer when call becomes active
426
461
  if (telnyxState === call_state_1.TelnyxCallState.ACTIVE && !this._startTime) {
427
462
  this._startDurationTimer();
@@ -7,6 +7,14 @@ export interface CredentialConfig {
7
7
  sipPassword: string;
8
8
  debug?: boolean;
9
9
  pushNotificationDeviceToken?: string;
10
+ /** Receive PushKit calls while the WebSocket session is active. Default: false */
11
+ pushWhenActive?: boolean;
12
+ /** Enable native missed call push notifications. Default: false */
13
+ enableMissedCallNotifications?: boolean;
14
+ /** Android only: name of a bundled `res/raw` ringtone (without its file extension). Falls back to the device ringtone. */
15
+ incomingCallRingtone?: string;
16
+ /** Enable Trickle ICE. Default: false */
17
+ useTrickleIce?: boolean;
10
18
  /** Enable automatic call quality reporting. Default: true */
11
19
  enableCallReports?: boolean;
12
20
  /** Stats collection interval in seconds. Default: 5 */
@@ -24,6 +32,14 @@ export interface TokenConfig {
24
32
  token: string;
25
33
  debug?: boolean;
26
34
  pushNotificationDeviceToken?: string;
35
+ /** Receive PushKit calls while the WebSocket session is active. Default: false */
36
+ pushWhenActive?: boolean;
37
+ /** Enable native missed call push notifications. Default: false */
38
+ enableMissedCallNotifications?: boolean;
39
+ /** Android only: name of a bundled `res/raw` ringtone (without its file extension). Falls back to the device ringtone. */
40
+ incomingCallRingtone?: string;
41
+ /** Enable Trickle ICE. Default: false */
42
+ useTrickleIce?: boolean;
27
43
  /** Enable automatic call quality reporting. Default: true */
28
44
  enableCallReports?: boolean;
29
45
  /** Stats collection interval in seconds. Default: 5 */
@@ -65,6 +81,7 @@ export declare function validateConfig(config: Config): string[];
65
81
  * @param options - Optional configuration settings
66
82
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
67
83
  * @param options.pushNotificationDeviceToken - Device token for push notifications
84
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
68
85
  * @returns Complete credential configuration object
69
86
  */
70
87
  export declare function createCredentialConfig(
@@ -79,6 +96,7 @@ export declare function createCredentialConfig(
79
96
  * @param options - Optional configuration settings
80
97
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
81
98
  * @param options.pushNotificationDeviceToken - Device token for push notifications
99
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
82
100
  * @returns Complete token configuration object
83
101
  */
84
102
  export declare function createTokenConfig(
@@ -62,6 +62,7 @@ function validateConfig(config) {
62
62
  * @param options - Optional configuration settings
63
63
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
64
64
  * @param options.pushNotificationDeviceToken - Device token for push notifications
65
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
65
66
  * @returns Complete credential configuration object
66
67
  */
67
68
  function createCredentialConfig(sipUser, sipPassword, options) {
@@ -79,6 +80,7 @@ function createCredentialConfig(sipUser, sipPassword, options) {
79
80
  * @param options - Optional configuration settings
80
81
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
81
82
  * @param options.pushNotificationDeviceToken - Device token for push notifications
83
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
82
84
  * @returns Complete token configuration object
83
85
  */
84
86
  function createTokenConfig(sipToken, options) {
@@ -6,7 +6,30 @@ const react_1 = require('react');
6
6
  const react_native_1 = require('react-native');
7
7
  const telnyx_voip_client_1 = require('./telnyx-voip-client');
8
8
  const connection_state_1 = require('./models/connection-state');
9
+ const call_state_1 = require('./models/call-state');
9
10
  const TelnyxVoiceContext_1 = require('./context/TelnyxVoiceContext');
11
+ let sharedBackgroundClient = null;
12
+ let sharedBackgroundDisposePromise = null;
13
+ const disposeSharedBackgroundClient = async (log, expectedClient) => {
14
+ if (sharedBackgroundDisposePromise) {
15
+ await sharedBackgroundDisposePromise;
16
+ }
17
+ const backgroundClient = expectedClient ?? sharedBackgroundClient;
18
+ if (!backgroundClient || (expectedClient && sharedBackgroundClient !== expectedClient)) {
19
+ return;
20
+ }
21
+ sharedBackgroundClient = null;
22
+ log('Disposing background client instance');
23
+ sharedBackgroundDisposePromise = backgroundClient
24
+ .dispose()
25
+ .catch((e) => {
26
+ log('Error disposing background client instance:', e);
27
+ })
28
+ .finally(() => {
29
+ sharedBackgroundDisposePromise = null;
30
+ });
31
+ await sharedBackgroundDisposePromise;
32
+ };
10
33
  /**
11
34
  * A comprehensive wrapper component that handles all Telnyx SDK lifecycle management.
12
35
  *
@@ -39,11 +62,17 @@ const TelnyxVoiceAppComponent = ({
39
62
  const [currentConnectionState, setCurrentConnectionState] = (0, react_1.useState)(
40
63
  voipClient.currentConnectionState
41
64
  );
65
+ // Refs that mirror mutable state for use inside long-lived effects and
66
+ // callbacks. Reading these flags through refs (instead of the state
67
+ // variables) keeps the setup effect's dependency array stable so
68
+ // connection/call subscriptions are not torn down and recreated every
69
+ // time the flags change — fixing the subscription gap described in
70
+ // VSDK-344.
71
+ const isHandlingForegroundCallRef = (0, react_1.useRef)(false);
72
+ const processingPushOnLaunchRef = (0, react_1.useRef)(false);
42
73
  // Refs for tracking state
43
74
  const appStateRef = (0, react_1.useRef)(react_native_1.AppState.currentState);
44
75
  const backgroundDetectorIgnore = (0, react_1.useRef)(false);
45
- // Static background client instance for singleton pattern
46
- const backgroundClientRef = (0, react_1.useRef)(null);
47
76
  const log = (0, react_1.useCallback)(
48
77
  (message, ...args) => {
49
78
  if (debug) {
@@ -59,7 +88,7 @@ const TelnyxVoiceAppComponent = ({
59
88
  appStateRef.current = nextAppState;
60
89
  log(`App state changed from ${previousAppState} to ${nextAppState}`);
61
90
  log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
62
- log(`Handling foreground call: ${isHandlingForegroundCall}`);
91
+ log(`Handling foreground call: ${isHandlingForegroundCallRef.current}`);
63
92
  // Call optional user callback first
64
93
  onAppStateChanged?.(nextAppState);
65
94
  // Only handle background disconnection when actually transitioning from active to background
@@ -87,12 +116,12 @@ const TelnyxVoiceAppComponent = ({
87
116
  await handleAppResumed();
88
117
  }
89
118
  },
90
- [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]
119
+ [enableAutoReconnect, onAppStateChanged, log]
91
120
  );
92
121
  // Handle app going to background - disconnect like the old implementation
93
122
  const handleAppBackgrounded = (0, react_1.useCallback)(async () => {
94
123
  // Check if we should ignore background detection (e.g., during active calls)
95
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
124
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCallRef.current) {
96
125
  log(
97
126
  'Background detector ignore flag set or handling foreground call - skipping disconnection'
98
127
  );
@@ -140,7 +169,7 @@ const TelnyxVoiceAppComponent = ({
140
169
  } catch (e) {
141
170
  log('Error disconnecting on background:', e);
142
171
  }
143
- }, [voipClient, isHandlingForegroundCall, log]);
172
+ }, [voipClient, log]);
144
173
  // Handle app resuming from background
145
174
  const handleAppResumed = (0, react_1.useCallback)(async () => {
146
175
  log('App resumed - checking reconnection needs');
@@ -148,7 +177,7 @@ const TelnyxVoiceAppComponent = ({
148
177
  // This handles the case where the user accepted a call while the app was backgrounded
149
178
  await checkForInitialPushNotification(true); // Pass true for fromAppResume
150
179
  // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
151
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
180
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCallRef.current) {
152
181
  log(
153
182
  'Background detector ignore flag set or handling foreground call - skipping reconnection'
154
183
  );
@@ -176,7 +205,7 @@ const TelnyxVoiceAppComponent = ({
176
205
  if (currentState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
177
206
  await attemptAutoReconnection();
178
207
  }
179
- }, [voipClient, isHandlingForegroundCall, log]);
208
+ }, [voipClient, log]);
180
209
  // Attempt to reconnect using stored credentials
181
210
  const attemptAutoReconnection = (0, react_1.useCallback)(async () => {
182
211
  try {
@@ -296,12 +325,13 @@ const TelnyxVoiceAppComponent = ({
296
325
  const checkForInitialPushNotification = (0, react_1.useCallback)(
297
326
  async (fromAppResume = false) => {
298
327
  log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
299
- if (processingPushOnLaunch && !fromAppResume) {
328
+ if (processingPushOnLaunchRef.current && !fromAppResume) {
300
329
  log('Already processing push, returning early');
301
330
  return;
302
331
  }
303
332
  if (!fromAppResume) {
304
333
  setProcessingPushOnLaunch(true);
334
+ processingPushOnLaunchRef.current = true;
305
335
  }
306
336
  onPushNotificationProcessingStarted?.();
307
337
  try {
@@ -324,11 +354,20 @@ const TelnyxVoiceAppComponent = ({
324
354
  // Prevent duplicate processing if already connected or connecting.
325
355
  // Since push data is no longer cleared on read, this guard prevents
326
356
  // re-processing when checkForInitialPushNotification fires again on app resume.
327
- if (
357
+ const isConnectedOrConnecting =
328
358
  voipClient.currentConnectionState ===
329
359
  connection_state_1.TelnyxConnectionState.CONNECTED ||
330
- voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTING
331
- ) {
360
+ voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTING;
361
+ const isActiveIOSCallWaitingPush =
362
+ react_native_1.Platform.OS === 'ios' &&
363
+ voipClient.currentConnectionState ===
364
+ connection_state_1.TelnyxConnectionState.CONNECTED &&
365
+ voipClient.currentCalls.some(
366
+ (call) =>
367
+ call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
368
+ call.currentState === call_state_1.TelnyxCallState.HELD
369
+ );
370
+ if (isConnectedOrConnecting && !isActiveIOSCallWaitingPush) {
332
371
  log(
333
372
  `SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
334
373
  );
@@ -336,20 +375,31 @@ const TelnyxVoiceAppComponent = ({
336
375
  }
337
376
  // Set flags to prevent auto-reconnection during push call
338
377
  setIsHandlingForegroundCall(true);
378
+ isHandlingForegroundCallRef.current = true;
339
379
  backgroundDetectorIgnore.current = true;
340
380
  log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
341
381
  log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
342
- disposeBackgroundClient();
382
+ await disposeBackgroundClient();
343
383
  // On iOS, coordinate with CallKit using the call_id from push metadata
344
384
  if (react_native_1.Platform.OS === 'ios') {
345
385
  const callId = pushData.metadata?.call_id;
346
386
  if (callId) {
347
387
  const { callKitCoordinator } = require('./callkit/callkit-coordinator');
348
388
  log('Notifying CallKit coordinator about push notification:', callId);
349
- await callKitCoordinator.handleCallKitPushReceived(callId, {
389
+ const processed = await callKitCoordinator.handleCallKitPushReceived(callId, {
350
390
  callData: { source: 'push_notification' },
351
391
  pushData: pushData,
352
392
  });
393
+ if (!processed) {
394
+ // The push was filtered, rejected, or otherwise ignored by
395
+ // CallKit. Reset the foreground-push lifecycle flags so the
396
+ // app does not stay in a state that skips background
397
+ // disconnect/reconnect handling.
398
+ log('CallKit push was not processed - resetting foreground flags');
399
+ setIsHandlingForegroundCall(false);
400
+ isHandlingForegroundCallRef.current = false;
401
+ backgroundDetectorIgnore.current = false;
402
+ }
353
403
  } else {
354
404
  log('No call_id found in push data, falling back to direct handling');
355
405
  await voipClient.handlePushNotification(pushData);
@@ -361,26 +411,18 @@ const TelnyxVoiceAppComponent = ({
361
411
  } catch (e) {
362
412
  log('Error processing initial push notification:', e);
363
413
  setIsHandlingForegroundCall(false);
414
+ isHandlingForegroundCallRef.current = false;
364
415
  } finally {
365
416
  setProcessingPushOnLaunch(false);
417
+ processingPushOnLaunchRef.current = false;
366
418
  onPushNotificationProcessingCompleted?.();
367
419
  }
368
420
  },
369
- [
370
- processingPushOnLaunch,
371
- voipClient,
372
- onPushNotificationProcessingStarted,
373
- onPushNotificationProcessingCompleted,
374
- log,
375
- ]
421
+ [voipClient, onPushNotificationProcessingStarted, onPushNotificationProcessingCompleted, log]
376
422
  );
377
423
  // Dispose background client instance when no longer needed
378
- const disposeBackgroundClient = (0, react_1.useCallback)(() => {
379
- if (backgroundClientRef.current) {
380
- log('Disposing background client instance');
381
- backgroundClientRef.current.dispose();
382
- backgroundClientRef.current = null;
383
- }
424
+ const disposeBackgroundClient = (0, react_1.useCallback)(async () => {
425
+ await disposeSharedBackgroundClient(log);
384
426
  }, [log]);
385
427
  // Create background client for push notification handling
386
428
  const createBackgroundClient = (0, react_1.useCallback)(() => {
@@ -429,17 +471,18 @@ const TelnyxVoiceAppComponent = ({
429
471
  }
430
472
  }
431
473
  log(
432
- `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
474
+ `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCallRef.current}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
433
475
  );
434
476
  if (
435
477
  !hasActiveWebRTCCalls &&
436
478
  !hasCallKitProcessing &&
437
- (isHandlingForegroundCall || backgroundDetectorIgnore.current)
479
+ (isHandlingForegroundCallRef.current || backgroundDetectorIgnore.current)
438
480
  ) {
439
481
  log(
440
482
  `No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`
441
483
  );
442
484
  setIsHandlingForegroundCall(false);
485
+ isHandlingForegroundCallRef.current = false;
443
486
  backgroundDetectorIgnore.current = false;
444
487
  } else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
445
488
  log(
@@ -450,9 +493,10 @@ const TelnyxVoiceAppComponent = ({
450
493
  }
451
494
  // Also reset processingPushOnLaunch if no calls are active
452
495
  // This ensures the flag doesn't get stuck after call ends
453
- if (calls.length === 0 && processingPushOnLaunch) {
496
+ if (calls.length === 0 && processingPushOnLaunchRef.current) {
454
497
  log('No active calls - resetting processing push flag');
455
498
  setProcessingPushOnLaunch(false);
499
+ processingPushOnLaunchRef.current = false;
456
500
  }
457
501
  });
458
502
  // Listen for immediate call action events from notification buttons (Android only)
@@ -501,7 +545,7 @@ const TelnyxVoiceAppComponent = ({
501
545
  // Handle initial push notification if app was launched from terminated state
502
546
  // Only check if we're not already processing to prevent infinite loops
503
547
  const timeoutId = setTimeout(() => {
504
- if (!processingPushOnLaunch) {
548
+ if (!processingPushOnLaunchRef.current) {
505
549
  checkForInitialPushNotification();
506
550
  }
507
551
  }, 100);
@@ -523,16 +567,9 @@ const TelnyxVoiceAppComponent = ({
523
567
  }
524
568
  clearTimeout(timeoutId);
525
569
  // Clean up background client instance
526
- disposeBackgroundClient();
570
+ void disposeBackgroundClient();
527
571
  };
528
- }, [
529
- voipClient,
530
- handleAppStateChange,
531
- disposeBackgroundClient,
532
- skipWebBackgroundDetection,
533
- isHandlingForegroundCall,
534
- log,
535
- ]);
572
+ }, [voipClient, handleAppStateChange, disposeBackgroundClient, skipWebBackgroundDetection, log]);
536
573
  // Simply return the children wrapped in context provider - all lifecycle management is handled internally
537
574
  return (0, jsx_runtime_1.jsx)(TelnyxVoiceContext_1.TelnyxVoiceProvider, {
538
575
  voipClient: voipClient,
@@ -608,18 +645,23 @@ const initializeAndCreate = async (options) => {
608
645
  */
609
646
  const handleBackgroundPush = async (message) => {
610
647
  console.log('[TelnyxVoiceApp] Background push received:', message);
648
+ let backgroundClient = null;
611
649
  try {
612
650
  // TODO: Initialize push notification service in isolate if needed
651
+ await disposeSharedBackgroundClient(console.log);
613
652
  // Use singleton pattern for background client to prevent multiple instances
614
- let backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
653
+ backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
615
654
  debug: false,
616
655
  });
656
+ sharedBackgroundClient = backgroundClient;
617
657
  await backgroundClient.handlePushNotification(message);
618
658
  console.log('[TelnyxVoiceApp] Background push processed successfully');
619
- // Clean up the background client
620
- backgroundClient.dispose();
621
659
  } catch (e) {
622
660
  console.log('[TelnyxVoiceApp] Error processing background push:', e);
661
+ } finally {
662
+ if (backgroundClient) {
663
+ await disposeSharedBackgroundClient(console.log, backgroundClient);
664
+ }
623
665
  }
624
666
  };
625
667
  // Create the component with static methods
@@ -2,6 +2,7 @@ import { Observable } from 'rxjs';
2
2
  import { TelnyxConnectionState } from './models/connection-state';
3
3
  import { Call } from './models/call';
4
4
  import { CredentialConfig, TokenConfig } from './models/config';
5
+ import { type CustomHeaders } from './internal/calls/call-state-controller';
5
6
  /**
6
7
  * Configuration options for TelnyxVoipClient
7
8
  */
@@ -10,6 +11,8 @@ export interface TelnyxVoipClientOptions {
10
11
  enableAppStateManagement?: boolean;
11
12
  /** Enable debug logging */
12
13
  debug?: boolean;
14
+ /** Enable Trickle ICE for calls created by this client */
15
+ useTrickleIce?: boolean;
13
16
  }
14
17
  /**
15
18
  * The main public interface for the react-voice-commons module.
@@ -26,7 +29,9 @@ export declare class TelnyxVoipClient {
26
29
  private readonly _sessionManager;
27
30
  private readonly _callStateController;
28
31
  private readonly _options;
32
+ private readonly _pendingCallKitAnswers;
29
33
  private _disposed;
34
+ private _disposePromise?;
30
35
  /**
31
36
  * Check if the app was launched from a push notification.
32
37
  *
@@ -100,6 +105,22 @@ export declare class TelnyxVoipClient {
100
105
  * ```
101
106
  */
102
107
  getCall(callId: string): Call | null;
108
+ /**
109
+ * Explicitly set the active call for multi-call scenarios.
110
+ * @param callId The ID of the call to mark as active
111
+ */
112
+ setActiveCall(callId: string): void;
113
+ /**
114
+ * Clear the explicitly selected active call and return to default selection.
115
+ */
116
+ clearActiveCall(): void;
117
+ /**
118
+ * Swap the current active call with a held call.
119
+ * On iOS this is coordinated through CallKit so native and SDK state stay aligned.
120
+ *
121
+ * @param targetCallId ID of the held call to make active
122
+ */
123
+ swapCalls(targetCallId: string): Promise<void>;
103
124
  /**
104
125
  * Current session ID (UUID) for this connection.
105
126
  */
@@ -159,7 +180,7 @@ export declare class TelnyxVoipClient {
159
180
  destination: string,
160
181
  callerName?: string,
161
182
  callerNumber?: string,
162
- customHeaders?: Record<string, string>
183
+ customHeaders?: CustomHeaders
163
184
  ): Promise<Call>;
164
185
  /**
165
186
  * Handle push notification payload.
@@ -195,12 +216,21 @@ export declare class TelnyxVoipClient {
195
216
  * This should be called when the user answers from CallKit before the socket connection is established
196
217
  * @param customHeaders Optional custom headers to include with the answer
197
218
  */
198
- queueAnswerFromCallKit(customHeaders?: Record<string, string>): void;
219
+ queueAnswerFromCallKit(
220
+ callKitUUIDOrHeaders?: string | Record<string, string>,
221
+ customHeaders?: Record<string, string>
222
+ ): void;
199
223
  /**
200
224
  * Queue an end action for when the call invite arrives (for CallKit integration)
201
225
  * This should be called when the user ends from CallKit before the socket connection is established
202
226
  */
203
- queueEndFromCallKit(): void;
227
+ queueEndFromCallKit(callKitUUID?: string): void;
228
+ /**
229
+ * Associate the next push-delivered INVITE with its app-facing CallKit UUID.
230
+ * The underlying signaling call ID remains unchanged.
231
+ * @internal
232
+ */
233
+ setPushNotificationCallKitUUID(callKitUUID: string | null): void;
204
234
  /**
205
235
  * Dispose of the client and clean up all resources.
206
236
  *
@@ -208,7 +238,20 @@ export declare class TelnyxVoipClient {
208
238
  * This is particularly important for background clients that should be
209
239
  * disposed after handling push notifications.
210
240
  */
211
- dispose(): void;
241
+ dispose(): Promise<void>;
242
+ /**
243
+ * Forward answers captured during cold start as soon as SessionManager has
244
+ * created the TelnyxRTC instance. SessionManager invokes its ready callback
245
+ * before connect(), so the UUID-keyed action is present when the INVITE
246
+ * arrives.
247
+ */
248
+ private _flushPendingCallKitAnswers;
249
+ /**
250
+ * Prefer an explicitly supplied token, otherwise hydrate it from PushKit's
251
+ * native storage. PushKit registration starts in AppDelegate before React
252
+ * mounts, so this removes the race between the JS token event and login.
253
+ */
254
+ private _withNativeVoipPushToken;
212
255
  /**
213
256
  * Store credential configuration for automatic reconnection
214
257
  */
@@ -236,7 +279,7 @@ export declare function createTelnyxVoipClient(options?: TelnyxVoipClientOptions
236
279
  * Disposes the current singleton so that a subsequent call to
237
280
  * `createTelnyxVoipClient()` will create a fresh instance.
238
281
  */
239
- export declare function destroyTelnyxVoipClient(): void;
282
+ export declare function destroyTelnyxVoipClient(): Promise<void>;
240
283
  /**
241
284
  * Create a new TelnyxVoipClient instance for background push notification handling.
242
285
  *