@telnyx/react-voice-commons-sdk 1.0.0 → 1.1.1

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 (43) 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/TelnyxNotificationHelper.kt +345 -24
  5. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +19 -0
  6. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +19 -0
  7. package/ios/CallKitBridge.m +27 -1
  8. package/ios/CallKitBridge.swift +640 -125
  9. package/ios/README.md +2 -2
  10. package/ios/VoicePnBridge.m +6 -0
  11. package/ios/VoicePnBridge.swift +15 -0
  12. package/lib/callkit/callkit-coordinator.d.ts +39 -3
  13. package/lib/callkit/callkit-coordinator.js +561 -148
  14. package/lib/callkit/callkit.d.ts +7 -0
  15. package/lib/callkit/callkit.js +83 -11
  16. package/lib/hooks/useAppStateHandler.js +6 -2
  17. package/lib/internal/CallKitHandler.d.ts +3 -0
  18. package/lib/internal/CallKitHandler.js +52 -46
  19. package/lib/internal/calls/call-state-controller.d.ts +33 -1
  20. package/lib/internal/calls/call-state-controller.js +100 -28
  21. package/lib/internal/session/session-manager.d.ts +15 -1
  22. package/lib/internal/session/session-manager.js +214 -62
  23. package/lib/internal/voice-pn-bridge.d.ts +18 -0
  24. package/lib/internal/voice-pn-bridge.js +39 -0
  25. package/lib/models/call.js +35 -0
  26. package/lib/models/config.d.ts +10 -0
  27. package/lib/models/config.js +2 -0
  28. package/lib/telnyx-voice-app.js +85 -43
  29. package/lib/telnyx-voip-client.d.ts +46 -5
  30. package/lib/telnyx-voip-client.js +238 -22
  31. package/package.json +7 -5
  32. package/src/callkit/callkit-coordinator.ts +642 -160
  33. package/src/callkit/callkit.ts +110 -12
  34. package/src/hooks/useAppStateHandler.ts +7 -2
  35. package/src/internal/CallKitHandler.tsx +60 -55
  36. package/src/internal/calls/call-state-controller.ts +112 -29
  37. package/src/internal/session/session-manager.ts +260 -65
  38. package/src/internal/voice-pn-bridge.ts +47 -0
  39. package/src/models/call.ts +34 -0
  40. package/src/models/config.ts +10 -0
  41. package/src/telnyx-voice-app.tsx +96 -45
  42. package/src/telnyx-voip-client.ts +216 -24
  43. package/src/types/telnyx-sdk.d.ts +2 -1
@@ -7,8 +7,12 @@ 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;
10
12
  /** Enable native missed call push notifications. Default: false */
11
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;
12
16
  /** Enable Trickle ICE. Default: false */
13
17
  useTrickleIce?: boolean;
14
18
  /** Enable automatic call quality reporting. Default: true */
@@ -29,8 +33,12 @@ export interface TokenConfig {
29
33
  token: string;
30
34
  debug?: boolean;
31
35
  pushNotificationDeviceToken?: string;
36
+ /** Receive PushKit calls while the WebSocket session is active. Default: false */
37
+ pushWhenActive?: boolean;
32
38
  /** Enable native missed call push notifications. Default: false */
33
39
  enableMissedCallNotifications?: boolean;
40
+ /** Android only: name of a bundled `res/raw` ringtone (without its file extension). Falls back to the device ringtone. */
41
+ incomingCallRingtone?: string;
34
42
  /** Enable Trickle ICE. Default: false */
35
43
  useTrickleIce?: boolean;
36
44
  /** Enable automatic call quality reporting. Default: true */
@@ -113,6 +121,7 @@ export function validateConfig(config: Config): string[] {
113
121
  * @param options - Optional configuration settings
114
122
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
115
123
  * @param options.pushNotificationDeviceToken - Device token for push notifications
124
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
116
125
  * @returns Complete credential configuration object
117
126
  */
118
127
  export function createCredentialConfig(
@@ -135,6 +144,7 @@ export function createCredentialConfig(
135
144
  * @param options - Optional configuration settings
136
145
  * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
137
146
  * @param options.pushNotificationDeviceToken - Device token for push notifications
147
+ * @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
138
148
  * @returns Complete token configuration object
139
149
  */
140
150
  export function createTokenConfig(
@@ -3,8 +3,40 @@ import { AppState, AppStateStatus, Platform } from 'react-native';
3
3
  import { TelnyxVoipClient, createBackgroundTelnyxVoipClient } from './telnyx-voip-client';
4
4
  import { TelnyxConnectionState } from './models/connection-state';
5
5
  import { Call } from './models/call';
6
+ import { TelnyxCallState } from './models/call-state';
6
7
  import { TelnyxVoiceProvider } from './context/TelnyxVoiceContext';
7
8
 
9
+ let sharedBackgroundClient: TelnyxVoipClient | null = null;
10
+ let sharedBackgroundDisposePromise: Promise<void> | null = null;
11
+
12
+ const disposeSharedBackgroundClient = async (
13
+ log: (message: string, ...args: any[]) => void,
14
+ expectedClient?: TelnyxVoipClient
15
+ ): Promise<void> => {
16
+ if (sharedBackgroundDisposePromise) {
17
+ await sharedBackgroundDisposePromise;
18
+ }
19
+
20
+ const backgroundClient = expectedClient ?? sharedBackgroundClient;
21
+ if (!backgroundClient || (expectedClient && sharedBackgroundClient !== expectedClient)) {
22
+ return;
23
+ }
24
+
25
+ sharedBackgroundClient = null;
26
+ log('Disposing background client instance');
27
+
28
+ sharedBackgroundDisposePromise = backgroundClient
29
+ .dispose()
30
+ .catch((e) => {
31
+ log('Error disposing background client instance:', e);
32
+ })
33
+ .finally(() => {
34
+ sharedBackgroundDisposePromise = null;
35
+ });
36
+
37
+ await sharedBackgroundDisposePromise;
38
+ };
39
+
8
40
  /**
9
41
  * Configuration options for TelnyxVoiceApp
10
42
  */
@@ -91,13 +123,19 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
91
123
  voipClient.currentConnectionState
92
124
  );
93
125
 
126
+ // Refs that mirror mutable state for use inside long-lived effects and
127
+ // callbacks. Reading these flags through refs (instead of the state
128
+ // variables) keeps the setup effect's dependency array stable so
129
+ // connection/call subscriptions are not torn down and recreated every
130
+ // time the flags change — fixing the subscription gap described in
131
+ // VSDK-344.
132
+ const isHandlingForegroundCallRef = useRef(false);
133
+ const processingPushOnLaunchRef = useRef(false);
134
+
94
135
  // Refs for tracking state
95
136
  const appStateRef = useRef<AppStateStatus>(AppState.currentState);
96
137
  const backgroundDetectorIgnore = useRef(false);
97
138
 
98
- // Static background client instance for singleton pattern
99
- const backgroundClientRef = useRef<TelnyxVoipClient | null>(null);
100
-
101
139
  const log = useCallback(
102
140
  (message: string, ...args: any[]) => {
103
141
  if (debug) {
@@ -115,7 +153,7 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
115
153
 
116
154
  log(`App state changed from ${previousAppState} to ${nextAppState}`);
117
155
  log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
118
- log(`Handling foreground call: ${isHandlingForegroundCall}`);
156
+ log(`Handling foreground call: ${isHandlingForegroundCallRef.current}`);
119
157
 
120
158
  // Call optional user callback first
121
159
  onAppStateChanged?.(nextAppState);
@@ -147,13 +185,13 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
147
185
  await handleAppResumed();
148
186
  }
149
187
  },
150
- [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]
188
+ [enableAutoReconnect, onAppStateChanged, log]
151
189
  );
152
190
 
153
191
  // Handle app going to background - disconnect like the old implementation
154
192
  const handleAppBackgrounded = useCallback(async () => {
155
193
  // Check if we should ignore background detection (e.g., during active calls)
156
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
194
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCallRef.current) {
157
195
  log(
158
196
  'Background detector ignore flag set or handling foreground call - skipping disconnection'
159
197
  );
@@ -206,7 +244,7 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
206
244
  } catch (e) {
207
245
  log('Error disconnecting on background:', e);
208
246
  }
209
- }, [voipClient, isHandlingForegroundCall, log]);
247
+ }, [voipClient, log]);
210
248
 
211
249
  // Handle app resuming from background
212
250
  const handleAppResumed = useCallback(async () => {
@@ -217,7 +255,7 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
217
255
  await checkForInitialPushNotification(true); // Pass true for fromAppResume
218
256
 
219
257
  // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
220
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
258
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCallRef.current) {
221
259
  log(
222
260
  'Background detector ignore flag set or handling foreground call - skipping reconnection'
223
261
  );
@@ -248,7 +286,7 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
248
286
  if (currentState !== TelnyxConnectionState.CONNECTED) {
249
287
  await attemptAutoReconnection();
250
288
  }
251
- }, [voipClient, isHandlingForegroundCall, log]);
289
+ }, [voipClient, log]);
252
290
 
253
291
  // Attempt to reconnect using stored credentials
254
292
  const attemptAutoReconnection = useCallback(async () => {
@@ -399,13 +437,14 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
399
437
  async (fromAppResume: boolean = false) => {
400
438
  log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
401
439
 
402
- if (processingPushOnLaunch && !fromAppResume) {
440
+ if (processingPushOnLaunchRef.current && !fromAppResume) {
403
441
  log('Already processing push, returning early');
404
442
  return;
405
443
  }
406
444
 
407
445
  if (!fromAppResume) {
408
446
  setProcessingPushOnLaunch(true);
447
+ processingPushOnLaunchRef.current = true;
409
448
  }
410
449
  onPushNotificationProcessingStarted?.();
411
450
 
@@ -433,10 +472,19 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
433
472
  // Prevent duplicate processing if already connected or connecting.
434
473
  // Since push data is no longer cleared on read, this guard prevents
435
474
  // re-processing when checkForInitialPushNotification fires again on app resume.
436
- if (
475
+ const isConnectedOrConnecting =
437
476
  voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED ||
438
- voipClient.currentConnectionState === TelnyxConnectionState.CONNECTING
439
- ) {
477
+ voipClient.currentConnectionState === TelnyxConnectionState.CONNECTING;
478
+ const isActiveIOSCallWaitingPush =
479
+ Platform.OS === 'ios' &&
480
+ voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED &&
481
+ voipClient.currentCalls.some(
482
+ (call) =>
483
+ call.currentState === TelnyxCallState.ACTIVE ||
484
+ call.currentState === TelnyxCallState.HELD
485
+ );
486
+
487
+ if (isConnectedOrConnecting && !isActiveIOSCallWaitingPush) {
440
488
  log(
441
489
  `SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
442
490
  );
@@ -445,11 +493,12 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
445
493
 
446
494
  // Set flags to prevent auto-reconnection during push call
447
495
  setIsHandlingForegroundCall(true);
496
+ isHandlingForegroundCallRef.current = true;
448
497
  backgroundDetectorIgnore.current = true;
449
498
  log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
450
499
  log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
451
500
 
452
- disposeBackgroundClient();
501
+ await disposeBackgroundClient();
453
502
 
454
503
  // On iOS, coordinate with CallKit using the call_id from push metadata
455
504
  if (Platform.OS === 'ios') {
@@ -457,10 +506,20 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
457
506
  if (callId) {
458
507
  const { callKitCoordinator } = require('./callkit/callkit-coordinator');
459
508
  log('Notifying CallKit coordinator about push notification:', callId);
460
- await callKitCoordinator.handleCallKitPushReceived(callId, {
509
+ const processed = await callKitCoordinator.handleCallKitPushReceived(callId, {
461
510
  callData: { source: 'push_notification' },
462
511
  pushData: pushData,
463
512
  });
513
+ if (!processed) {
514
+ // The push was filtered, rejected, or otherwise ignored by
515
+ // CallKit. Reset the foreground-push lifecycle flags so the
516
+ // app does not stay in a state that skips background
517
+ // disconnect/reconnect handling.
518
+ log('CallKit push was not processed - resetting foreground flags');
519
+ setIsHandlingForegroundCall(false);
520
+ isHandlingForegroundCallRef.current = false;
521
+ backgroundDetectorIgnore.current = false;
522
+ }
464
523
  } else {
465
524
  log('No call_id found in push data, falling back to direct handling');
466
525
  await voipClient.handlePushNotification(pushData);
@@ -473,27 +532,19 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
473
532
  } catch (e) {
474
533
  log('Error processing initial push notification:', e);
475
534
  setIsHandlingForegroundCall(false);
535
+ isHandlingForegroundCallRef.current = false;
476
536
  } finally {
477
537
  setProcessingPushOnLaunch(false);
538
+ processingPushOnLaunchRef.current = false;
478
539
  onPushNotificationProcessingCompleted?.();
479
540
  }
480
541
  },
481
- [
482
- processingPushOnLaunch,
483
- voipClient,
484
- onPushNotificationProcessingStarted,
485
- onPushNotificationProcessingCompleted,
486
- log,
487
- ]
542
+ [voipClient, onPushNotificationProcessingStarted, onPushNotificationProcessingCompleted, log]
488
543
  );
489
544
 
490
545
  // Dispose background client instance when no longer needed
491
- const disposeBackgroundClient = useCallback(() => {
492
- if (backgroundClientRef.current) {
493
- log('Disposing background client instance');
494
- backgroundClientRef.current.dispose();
495
- backgroundClientRef.current = null;
496
- }
546
+ const disposeBackgroundClient = useCallback(async (): Promise<void> => {
547
+ await disposeSharedBackgroundClient(log);
497
548
  }, [log]);
498
549
 
499
550
  // Create background client for push notification handling
@@ -551,18 +602,19 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
551
602
  }
552
603
 
553
604
  log(
554
- `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
605
+ `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCallRef.current}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
555
606
  );
556
607
 
557
608
  if (
558
609
  !hasActiveWebRTCCalls &&
559
610
  !hasCallKitProcessing &&
560
- (isHandlingForegroundCall || backgroundDetectorIgnore.current)
611
+ (isHandlingForegroundCallRef.current || backgroundDetectorIgnore.current)
561
612
  ) {
562
613
  log(
563
614
  `No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`
564
615
  );
565
616
  setIsHandlingForegroundCall(false);
617
+ isHandlingForegroundCallRef.current = false;
566
618
  backgroundDetectorIgnore.current = false;
567
619
  } else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
568
620
  log(
@@ -574,9 +626,10 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
574
626
 
575
627
  // Also reset processingPushOnLaunch if no calls are active
576
628
  // This ensures the flag doesn't get stuck after call ends
577
- if (calls.length === 0 && processingPushOnLaunch) {
629
+ if (calls.length === 0 && processingPushOnLaunchRef.current) {
578
630
  log('No active calls - resetting processing push flag');
579
631
  setProcessingPushOnLaunch(false);
632
+ processingPushOnLaunchRef.current = false;
580
633
  }
581
634
  });
582
635
 
@@ -628,7 +681,7 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
628
681
  // Handle initial push notification if app was launched from terminated state
629
682
  // Only check if we're not already processing to prevent infinite loops
630
683
  const timeoutId = setTimeout(() => {
631
- if (!processingPushOnLaunch) {
684
+ if (!processingPushOnLaunchRef.current) {
632
685
  checkForInitialPushNotification();
633
686
  }
634
687
  }, 100);
@@ -651,16 +704,9 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
651
704
  }
652
705
  clearTimeout(timeoutId);
653
706
  // Clean up background client instance
654
- disposeBackgroundClient();
707
+ void disposeBackgroundClient();
655
708
  };
656
- }, [
657
- voipClient,
658
- handleAppStateChange,
659
- disposeBackgroundClient,
660
- skipWebBackgroundDetection,
661
- isHandlingForegroundCall,
662
- log,
663
- ]);
709
+ }, [voipClient, handleAppStateChange, disposeBackgroundClient, skipWebBackgroundDetection, log]);
664
710
 
665
711
  // Simply return the children wrapped in context provider - all lifecycle management is handled internally
666
712
  return <TelnyxVoiceProvider voipClient={voipClient}>{children}</TelnyxVoiceProvider>;
@@ -769,23 +815,28 @@ const initializeAndCreate = async (options: {
769
815
  */
770
816
  const handleBackgroundPush = async (message: any): Promise<void> => {
771
817
  console.log('[TelnyxVoiceApp] Background push received:', message);
818
+ let backgroundClient: TelnyxVoipClient | null = null;
772
819
 
773
820
  try {
774
821
  // TODO: Initialize push notification service in isolate if needed
775
822
 
823
+ await disposeSharedBackgroundClient(console.log);
824
+
776
825
  // Use singleton pattern for background client to prevent multiple instances
777
- let backgroundClient = createBackgroundTelnyxVoipClient({
826
+ backgroundClient = createBackgroundTelnyxVoipClient({
778
827
  debug: false,
779
828
  });
829
+ sharedBackgroundClient = backgroundClient;
780
830
 
781
831
  await backgroundClient.handlePushNotification(message);
782
832
 
783
833
  console.log('[TelnyxVoiceApp] Background push processed successfully');
784
-
785
- // Clean up the background client
786
- backgroundClient.dispose();
787
834
  } catch (e) {
788
835
  console.log('[TelnyxVoiceApp] Error processing background push:', e);
836
+ } finally {
837
+ if (backgroundClient) {
838
+ await disposeSharedBackgroundClient(console.log, backgroundClient);
839
+ }
789
840
  }
790
841
  };
791
842