@telnyx/react-voice-commons-sdk 0.1.7-beta.1 → 0.1.8-beta.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.1.7](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.7) (2026-02-20)
4
+
5
+ ### Enhancement
6
+
7
+ • Added `TelnyxVoipClient.isLaunchedFromPushNotification()` static method to check if the app was cold-started from a push notification, allowing consumers to skip auto-login and avoid double-login races
8
+ • `createTelnyxVoipClient()` now returns a singleton — safe to call inside React component bodies without creating a new instance on every render
9
+ • Added `destroyTelnyxVoipClient()` to tear down the singleton when a fresh instance is needed
10
+ • `TelnyxVoiceApp` now automatically wires the `voipClient` on the CallKit coordinator on mount — consumers no longer need to manually call `setVoipClient()` at the correct component level
11
+
12
+ ### Bug Fixing
13
+
14
+ • Fixed cold-start push notification failures caused by double-login race between user auto-login and SDK internal push login
15
+ • Fixed CallKit coordinator having no `voipClient` reference when user answered a call via CallKit before navigating to the correct screen
16
+ • Fixed `call_id` extraction in `checkForInitialPushNotification` — the double-nested path `pushData.metadata?.metadata?.call_id` never resolved, so the CallKit coordinator was bypassed on iOS
17
+ • Refactored `checkForInitialPushNotification` into `getAndroidPushData` and `getIOSPushData` helpers to reduce nesting and improve readability
18
+
19
+ ### Deprecation
20
+
21
+ • `setVoipClient()` on `CallKitCoordinator` and `useCallKitCoordinator()` hook is now deprecated — `TelnyxVoiceApp` handles this automatically
22
+
23
+ ## [0.1.7-beta.0](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.7-beta.0) (2026-02-18)
24
+
25
+ ### Bug Fixing
26
+
27
+ • Fixed `call_id` extraction in `checkForInitialPushNotification` — the double-nested path `pushData.metadata?.metadata?.call_id` never resolved, so the CallKit coordinator was bypassed and all iOS push calls fell through to direct handling
28
+ • Refactored `checkForInitialPushNotification` into `getAndroidPushData` and `getIOSPushData` helpers to reduce nesting and improve readability
29
+
3
30
  ## [0.1.6](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.6) (2025-12-09)
4
31
 
5
32
  ### Enhancement
package/README.md CHANGED
@@ -32,15 +32,30 @@ The `@telnyx/react-voice-commons-sdk` library provides:
32
32
  Integrate the library using the `TelnyxVoiceApp` component for automatic lifecycle management:
33
33
 
34
34
  ```tsx
35
- import { TelnyxVoiceApp, createTelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
35
+ import {
36
+ TelnyxVoiceApp,
37
+ TelnyxVoipClient,
38
+ createTelnyxVoipClient,
39
+ } from '@telnyx/react-voice-commons-sdk';
36
40
 
37
- // Create the VoIP client instance
41
+ // Create the VoIP client instance (singleton — safe to call inside a component body)
38
42
  const voipClient = createTelnyxVoipClient({
39
43
  enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
40
44
  debug: true, // Optional: Enable debug logging
41
45
  });
42
46
 
43
47
  export default function App() {
48
+ // Skip auto-login if the app was launched from a push notification —
49
+ // the SDK handles login internally via the push notification flow.
50
+ React.useEffect(() => {
51
+ TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
52
+ if (!isFromPush) {
53
+ // Safe to auto-login
54
+ voipClient.loginFromStoredConfig();
55
+ }
56
+ });
57
+ }, []);
58
+
44
59
  return (
45
60
  <TelnyxVoiceApp voipClient={voipClient} enableAutoReconnect={false} debug={true}>
46
61
  <YourAppContent />
@@ -54,10 +69,16 @@ export default function App() {
54
69
  ### 1. VoIP Client Configuration
55
70
 
56
71
  ```tsx
72
+ // createTelnyxVoipClient is a singleton — repeated calls return the same instance.
73
+ // This makes it safe to call inside a React component body without re-creating on every render.
57
74
  const voipClient = createTelnyxVoipClient({
58
75
  enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
59
76
  debug: true, // Optional: Enable debug logging
60
77
  });
78
+
79
+ // If you need to tear down and recreate the client (e.g., on logout):
80
+ import { destroyTelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
81
+ destroyTelnyxVoipClient(); // Disposes the singleton; next createTelnyxVoipClient() call creates a fresh instance
61
82
  ```
62
83
 
63
84
  **Configuration Options Explained:**
@@ -73,6 +94,7 @@ The `TelnyxVoiceApp` component handles:
73
94
  - Push notification processing from terminated state
74
95
  - Login state management with automatic reconnection
75
96
  - Background client management for push notifications
97
+ - **Automatic CallKit coordinator wiring** — the `voipClient` is set on the CallKit coordinator on mount, so you don't need to call `setVoipClient()` manually
76
98
 
77
99
  ### 3. Reactive State Management
78
100
 
@@ -416,9 +438,18 @@ npx expo run:ios
416
438
 
417
439
  ### Common Integration Issues
418
440
 
419
- ### Double Login
441
+ ### Double Login on Cold-Start
442
+
443
+ When the app is launched from a push notification, the SDK handles login internally. If your app also auto-logs in on mount, both will race and the push flow breaks. Use `isLaunchedFromPushNotification()` to guard your auto-login:
444
+
445
+ ```tsx
446
+ const isFromPush = await TelnyxVoipClient.isLaunchedFromPushNotification();
447
+ if (!isFromPush) {
448
+ voipClient.loginFromStoredConfig();
449
+ }
450
+ ```
420
451
 
421
- Ensure you're not calling login methods manually when using `TelnyxVoiceApp` with auto-reconnection enabled.
452
+ Also ensure you're not calling login methods manually when using `TelnyxVoiceApp` with auto-reconnection enabled.
422
453
 
423
454
  ### Background Disconnection
424
455
 
@@ -457,7 +488,6 @@ useEffect(() => {
457
488
  const subscription = voipClient.connectionState$.subscribe(handleStateChange);
458
489
  return () => subscription.unsubscribe();
459
490
  }, []);
460
- }, []);
461
491
  ```
462
492
 
463
493
  ## Documentation
@@ -303,10 +303,21 @@ class CallKitCoordinator {
303
303
  } else {
304
304
  console.log('CallKitCoordinator: Outgoing call, skipping answer and CONNECTING state');
305
305
  }
306
+ // Clear push data now that answer action is fulfilled
307
+ try {
308
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
309
+ console.log('CallKitCoordinator: Cleared pending VoIP push after answer fulfilled');
310
+ } catch (clearErr) {
311
+ console.error('CallKitCoordinator: Error clearing push data after answer:', clearErr);
312
+ }
306
313
  } catch (error) {
307
314
  console.error('CallKitCoordinator: Error processing CallKit answer', error);
308
315
  await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
309
316
  this.cleanupCall(callKitUUID);
317
+ // Clear push data even on error to prevent stale state
318
+ try {
319
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
320
+ } catch (_) {}
310
321
  } finally {
311
322
  this.processingCalls.delete(callKitUUID);
312
323
  }
@@ -352,6 +363,11 @@ class CallKitCoordinator {
352
363
  } finally {
353
364
  this.processingCalls.delete(callKitUUID);
354
365
  this.cleanupCall(callKitUUID);
366
+ // Clear push data now that end action is fulfilled
367
+ try {
368
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
369
+ console.log('CallKitCoordinator: Cleared pending VoIP push after end fulfilled');
370
+ } catch (_) {}
355
371
  // Check if app is in background and no more calls - disconnect client
356
372
  await this.checkBackgroundDisconnection();
357
373
  }
@@ -506,6 +522,11 @@ class CallKitCoordinator {
506
522
  // Set the pending push action to be handled when app comes to foreground
507
523
  await voice_pn_bridge_1.VoicePnBridge.setPendingPushAction(pushAction, pushMetadata);
508
524
  console.log('CallKitCoordinator: ✅ Set pending push action');
525
+ // Clear push data now that push notification answer is handled
526
+ try {
527
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
528
+ console.log('CallKitCoordinator: Cleared pending VoIP push after push answer handled');
529
+ } catch (_) {}
509
530
  return;
510
531
  }
511
532
  // For other platforms (shouldn't happen on iOS)
@@ -533,6 +554,11 @@ class CallKitCoordinator {
533
554
  this.voipClient.queueEndFromCallKit();
534
555
  // Clean up push notification state
535
556
  await this.cleanupPushNotificationState();
557
+ // Clear push data now that rejection is handled
558
+ try {
559
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
560
+ console.log('CallKitCoordinator: Cleared pending VoIP push after rejection handled');
561
+ } catch (_) {}
536
562
  console.log('CallKitCoordinator: 🎯 Push notification rejection handling complete');
537
563
  return;
538
564
  }
@@ -738,7 +764,11 @@ class CallKitCoordinator {
738
764
  * This helps prevent premature flag resets during CallKit operations
739
765
  */
740
766
  hasProcessingCalls() {
741
- return this.processingCalls.size > 0;
767
+ // Also return true when isCallFromPush is set — this prevents the
768
+ // calls$ subscription in TelnyxVoiceApp from resetting protection flags
769
+ // (isHandlingForegroundCall, backgroundDetectorIgnore) before the WebRTC
770
+ // call arrives during push notification handling.
771
+ return this.processingCalls.size > 0 || this.isCallFromPush;
742
772
  }
743
773
  /**
744
774
  * Check if there's currently a call from push notification being processed
@@ -326,8 +326,8 @@ class CallStateController {
326
326
  console.log('CallStateController: Reporting incoming call to CallKitCoordinator');
327
327
  callkit_coordinator_1.callKitCoordinator.reportIncomingCall(
328
328
  telnyxCall,
329
- call.destination,
330
- call.destination
329
+ call.callerName,
330
+ call.callerNumber
331
331
  );
332
332
  } else {
333
333
  // Handle outgoing call with CallKit
@@ -49,7 +49,9 @@ export declare class SessionManager {
49
49
  */
50
50
  disconnect(): Promise<void>;
51
51
  /**
52
- * Disable push notifications for the current session
52
+ * Disable push notifications for the current session.
53
+ * Delegates to the TelnyxRTC client's disablePushNotification() method
54
+ * which sends a 'telnyx_rtc.disable_push_notification' message via the socket.
53
55
  */
54
56
  disablePushNotifications(): void;
55
57
  /**
@@ -140,16 +140,19 @@ class SessionManager {
140
140
  this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
141
141
  }
142
142
  /**
143
- * Disable push notifications for the current session
143
+ * Disable push notifications for the current session.
144
+ * Delegates to the TelnyxRTC client's disablePushNotification() method
145
+ * which sends a 'telnyx_rtc.disable_push_notification' message via the socket.
144
146
  */
145
147
  disablePushNotifications() {
146
148
  if (
147
149
  this._telnyxClient &&
148
150
  this.currentState === connection_state_1.TelnyxConnectionState.CONNECTED
149
151
  ) {
150
- // Implementation depends on the actual Telnyx SDK API
151
- // This is a placeholder for the actual implementation
152
- console.log('Disabling push notifications for session:', this._sessionId);
152
+ console.log('SessionManager: Disabling push notifications for session:', this._sessionId);
153
+ this._telnyxClient.disablePushNotification();
154
+ } else {
155
+ console.warn('SessionManager: Cannot disable push - client not connected');
153
156
  }
154
157
  }
155
158
  /**
@@ -46,6 +46,16 @@ export declare class Call {
46
46
  * Whether this is an outgoing call
47
47
  */
48
48
  get isOutgoing(): boolean;
49
+ /**
50
+ * The original caller name (from_display_name) received in the INVITE message.
51
+ * Falls back to destination if not available.
52
+ */
53
+ get callerName(): string;
54
+ /**
55
+ * The original caller number received in the INVITE message.
56
+ * Falls back to destination if not available.
57
+ */
58
+ get callerNumber(): string;
49
59
  /**
50
60
  * Current call state (synchronous access)
51
61
  */
@@ -115,6 +115,20 @@ class Call {
115
115
  get isOutgoing() {
116
116
  return !this._isIncoming;
117
117
  }
118
+ /**
119
+ * The original caller name (from_display_name) received in the INVITE message.
120
+ * Falls back to destination if not available.
121
+ */
122
+ get callerName() {
123
+ return this._originalCallerName || this._destination;
124
+ }
125
+ /**
126
+ * The original caller number received in the INVITE message.
127
+ * Falls back to destination if not available.
128
+ */
129
+ get callerNumber() {
130
+ return this._originalCallerNumber || this._destination;
131
+ }
118
132
  /**
119
133
  * Current call state (synchronous access)
120
134
  */
@@ -274,8 +274,14 @@ const TelnyxVoiceAppComponent = ({
274
274
  return null;
275
275
  }
276
276
  log('Found pending VoIP push data:', voipPayload);
277
- await VoicePnBridge.clearPendingVoipPush();
278
- log('Cleared pending VoIP push data after retrieval');
277
+ // Do NOT clear push data here. Let it persist until the answer/end action
278
+ // is fulfilled in the CallKit coordinator. This prevents a race condition in
279
+ // Expo apps where the RN bridge mounts immediately on push notification —
280
+ // the push data would be consumed and cleared before the user answers,
281
+ // leaving the coordinator with nothing to work with.
282
+ // For non-Expo apps (RN mounts after answer), the coordinator's
283
+ // handlePushNotificationAnswer/Reject clears the data before
284
+ // checkForInitialPushNotification ever runs, so no loop occurs.
279
285
  return { action: 'incoming_call', metadata: voipPayload.metadata, from_notification: true };
280
286
  } catch (parseError) {
281
287
  log('Error parsing VoIP push JSON:', parseError);
@@ -315,11 +321,17 @@ const TelnyxVoiceAppComponent = ({
315
321
  return;
316
322
  }
317
323
  log('Processing initial push notification...');
318
- // Prevent duplicate processing if already connected
324
+ // Prevent duplicate processing if already connected or connecting.
325
+ // Since push data is no longer cleared on read, this guard prevents
326
+ // re-processing when checkForInitialPushNotification fires again on app resume.
319
327
  if (
320
- voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTED
328
+ voipClient.currentConnectionState ===
329
+ connection_state_1.TelnyxConnectionState.CONNECTED ||
330
+ voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTING
321
331
  ) {
322
- log('SKIPPING - Already connected, preventing duplicate processing');
332
+ log(
333
+ `SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
334
+ );
323
335
  return;
324
336
  }
325
337
  // Set flags to prevent auto-reconnection during push call
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "0.1.7-beta.1",
3
+ "version": "0.1.8-beta.0",
4
4
  "description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",
@@ -306,10 +306,21 @@ class CallKitCoordinator {
306
306
  } else {
307
307
  console.log('CallKitCoordinator: Outgoing call, skipping answer and CONNECTING state');
308
308
  }
309
+ // Clear push data now that answer action is fulfilled
310
+ try {
311
+ await VoicePnBridge.clearPendingVoipPush();
312
+ console.log('CallKitCoordinator: Cleared pending VoIP push after answer fulfilled');
313
+ } catch (clearErr) {
314
+ console.error('CallKitCoordinator: Error clearing push data after answer:', clearErr);
315
+ }
309
316
  } catch (error) {
310
317
  console.error('CallKitCoordinator: Error processing CallKit answer', error);
311
318
  await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
312
319
  this.cleanupCall(callKitUUID);
320
+ // Clear push data even on error to prevent stale state
321
+ try {
322
+ await VoicePnBridge.clearPendingVoipPush();
323
+ } catch (_) {}
313
324
  } finally {
314
325
  this.processingCalls.delete(callKitUUID);
315
326
  }
@@ -366,6 +377,12 @@ class CallKitCoordinator {
366
377
  this.processingCalls.delete(callKitUUID);
367
378
  this.cleanupCall(callKitUUID);
368
379
 
380
+ // Clear push data now that end action is fulfilled
381
+ try {
382
+ await VoicePnBridge.clearPendingVoipPush();
383
+ console.log('CallKitCoordinator: Cleared pending VoIP push after end fulfilled');
384
+ } catch (_) {}
385
+
369
386
  // Check if app is in background and no more calls - disconnect client
370
387
  await this.checkBackgroundDisconnection();
371
388
  }
@@ -544,6 +561,12 @@ class CallKitCoordinator {
544
561
  await VoicePnBridge.setPendingPushAction(pushAction, pushMetadata);
545
562
  console.log('CallKitCoordinator: ✅ Set pending push action');
546
563
 
564
+ // Clear push data now that push notification answer is handled
565
+ try {
566
+ await VoicePnBridge.clearPendingVoipPush();
567
+ console.log('CallKitCoordinator: Cleared pending VoIP push after push answer handled');
568
+ } catch (_) {}
569
+
547
570
  return;
548
571
  }
549
572
 
@@ -577,6 +600,12 @@ class CallKitCoordinator {
577
600
  // Clean up push notification state
578
601
  await this.cleanupPushNotificationState();
579
602
 
603
+ // Clear push data now that rejection is handled
604
+ try {
605
+ await VoicePnBridge.clearPendingVoipPush();
606
+ console.log('CallKitCoordinator: Cleared pending VoIP push after rejection handled');
607
+ } catch (_) {}
608
+
580
609
  console.log('CallKitCoordinator: 🎯 Push notification rejection handling complete');
581
610
  return;
582
611
  }
@@ -815,7 +844,11 @@ class CallKitCoordinator {
815
844
  * This helps prevent premature flag resets during CallKit operations
816
845
  */
817
846
  hasProcessingCalls(): boolean {
818
- return this.processingCalls.size > 0;
847
+ // Also return true when isCallFromPush is set — this prevents the
848
+ // calls$ subscription in TelnyxVoiceApp from resetting protection flags
849
+ // (isHandlingForegroundCall, backgroundDetectorIgnore) before the WebRTC
850
+ // call arrives during push notification handling.
851
+ return this.processingCalls.size > 0 || this.isCallFromPush;
819
852
  }
820
853
 
821
854
  /**
@@ -392,7 +392,7 @@ export class CallStateController {
392
392
  } else if (call.isIncoming) {
393
393
  // Handle incoming call with CallKit (only if not already integrated)
394
394
  console.log('CallStateController: Reporting incoming call to CallKitCoordinator');
395
- callKitCoordinator.reportIncomingCall(telnyxCall, call.destination, call.destination);
395
+ callKitCoordinator.reportIncomingCall(telnyxCall, call.callerName, call.callerNumber);
396
396
  } else {
397
397
  // Handle outgoing call with CallKit
398
398
  console.log('CallStateController: Starting outgoing call with CallKitCoordinator');
@@ -111,13 +111,16 @@ export class SessionManager {
111
111
  }
112
112
 
113
113
  /**
114
- * Disable push notifications for the current session
114
+ * Disable push notifications for the current session.
115
+ * Delegates to the TelnyxRTC client's disablePushNotification() method
116
+ * which sends a 'telnyx_rtc.disable_push_notification' message via the socket.
115
117
  */
116
118
  disablePushNotifications(): void {
117
119
  if (this._telnyxClient && this.currentState === TelnyxConnectionState.CONNECTED) {
118
- // Implementation depends on the actual Telnyx SDK API
119
- // This is a placeholder for the actual implementation
120
- console.log('Disabling push notifications for session:', this._sessionId);
120
+ console.log('SessionManager: Disabling push notifications for session:', this._sessionId);
121
+ this._telnyxClient.disablePushNotification();
122
+ } else {
123
+ console.warn('SessionManager: Cannot disable push - client not connected');
121
124
  }
122
125
  }
123
126
 
@@ -66,6 +66,22 @@ export class Call {
66
66
  return !this._isIncoming;
67
67
  }
68
68
 
69
+ /**
70
+ * The original caller name (from_display_name) received in the INVITE message.
71
+ * Falls back to destination if not available.
72
+ */
73
+ get callerName(): string {
74
+ return this._originalCallerName || this._destination;
75
+ }
76
+
77
+ /**
78
+ * The original caller number received in the INVITE message.
79
+ * Falls back to destination if not available.
80
+ */
81
+ get callerNumber(): string {
82
+ return this._originalCallerNumber || this._destination;
83
+ }
84
+
69
85
  /**
70
86
  * Current call state (synchronous access)
71
87
  */
@@ -374,8 +374,14 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
374
374
 
375
375
  log('Found pending VoIP push data:', voipPayload);
376
376
 
377
- await VoicePnBridge.clearPendingVoipPush();
378
- log('Cleared pending VoIP push data after retrieval');
377
+ // Do NOT clear push data here. Let it persist until the answer/end action
378
+ // is fulfilled in the CallKit coordinator. This prevents a race condition in
379
+ // Expo apps where the RN bridge mounts immediately on push notification —
380
+ // the push data would be consumed and cleared before the user answers,
381
+ // leaving the coordinator with nothing to work with.
382
+ // For non-Expo apps (RN mounts after answer), the coordinator's
383
+ // handlePushNotificationAnswer/Reject clears the data before
384
+ // checkForInitialPushNotification ever runs, so no loop occurs.
379
385
 
380
386
  return { action: 'incoming_call', metadata: voipPayload.metadata, from_notification: true };
381
387
  } catch (parseError) {
@@ -424,9 +430,14 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
424
430
 
425
431
  log('Processing initial push notification...');
426
432
 
427
- // Prevent duplicate processing if already connected
428
- if (voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED) {
429
- log('SKIPPING - Already connected, preventing duplicate processing');
433
+ // Prevent duplicate processing if already connected or connecting.
434
+ // Since push data is no longer cleared on read, this guard prevents
435
+ // re-processing when checkForInitialPushNotification fires again on app resume.
436
+ if (
437
+ voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED ||
438
+ voipClient.currentConnectionState === TelnyxConnectionState.CONNECTING
439
+ ) {
440
+ log(`SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`);
430
441
  return;
431
442
  }
432
443
 
@@ -83,6 +83,7 @@ declare module '@telnyx/react-native-voice-sdk' {
83
83
  connect(): Promise<void>;
84
84
  disconnect(): void;
85
85
  newCall(options: CallOptions): Promise<Call>;
86
+ disablePushNotification(): void;
86
87
 
87
88
  on(event: string, listener: (...args: any[]) => void): this;
88
89
  off(event: string, listener: (...args: any[]) => void): this;