@telnyx/react-voice-commons-sdk 0.2.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.2.1](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/commons-sdk-v0.2.1) (2026-04-02)
4
+
5
+ ### Bug Fixing
6
+
7
+ - Fixed CallKit auto-answer not working on iOS when user answers from CallKit UI during app cold launch from VoIP push. The native `CXAnswerCallAction` event was silently dropped because JS listeners weren't registered yet. Now persists the answer action in UserDefaults so the JS side can detect and honor it.
8
+
3
9
  ## [0.2.0](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/commons-sdk-v0.2.0) (2026-04-01)
4
10
 
5
11
  ### Enhancement
@@ -533,6 +533,12 @@ import React
533
533
  NSLog("📞 TelnyxVoice: CALLKIT ANSWER ACTION - Provider: \(provider), Action: \(action)")
534
534
  NSLog("TelnyxVoice: User answered call with UUID: \(action.callUUID)")
535
535
 
536
+ // Always persist the answer action in UserDefaults so the JS side can detect it
537
+ // even when the RCTEventEmitter bridge is not yet ready (app cold-launched from push).
538
+ UserDefaults.standard.set(action.callUUID.uuidString, forKey: "pending_callkit_answer")
539
+ UserDefaults.standard.synchronize()
540
+ NSLog("TelnyxVoice: Stored pending CallKit answer for UUID: \(action.callUUID)")
541
+
536
542
  // Check if this is a programmatic answer (call already answered in WebRTC)
537
543
  // vs a user answer from CallKit UI
538
544
  if let callData = activeCalls[action.callUUID],
@@ -555,6 +561,10 @@ import React
555
561
  public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
556
562
  NSLog("TelnyxVoice: User ended call with UUID: \(action.callUUID)")
557
563
 
564
+ // Clear any pending CallKit answer (prevents stale auto-answer on next call)
565
+ UserDefaults.standard.removeObject(forKey: "pending_callkit_answer")
566
+ UserDefaults.standard.synchronize()
567
+
558
568
  // Notify React Native via CallKit bridge
559
569
  CallKitBridge.shared?.emitCallEvent(
560
570
  "CallKitDidPerformEndCallAction", callUUID: action.callUUID,
@@ -22,6 +22,12 @@ RCT_EXTERN_METHOD(getPendingVoipPush:(RCTPromiseResolveBlock)resolve
22
22
  RCT_EXTERN_METHOD(clearPendingVoipPush:(RCTPromiseResolveBlock)resolve
23
23
  rejecter:(RCTPromiseRejectBlock)reject)
24
24
 
25
+ RCT_EXTERN_METHOD(getPendingCallKitAnswer:(RCTPromiseResolveBlock)resolve
26
+ rejecter:(RCTPromiseRejectBlock)reject)
27
+
28
+ RCT_EXTERN_METHOD(clearPendingCallKitAnswer:(RCTPromiseResolveBlock)resolve
29
+ rejecter:(RCTPromiseRejectBlock)reject)
30
+
25
31
  RCT_EXTERN_METHOD(getPendingVoipAction:(RCTPromiseResolveBlock)resolve
26
32
  rejecter:(RCTPromiseRejectBlock)reject)
27
33
 
@@ -70,6 +70,21 @@ class VoicePnBridge: NSObject {
70
70
  resolve(true)
71
71
  }
72
72
 
73
+ @objc
74
+ func getPendingCallKitAnswer(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
75
+ let answer = UserDefaults.standard.string(forKey: "pending_callkit_answer")
76
+ NSLog("[VoicePnBridge] getPendingCallKitAnswer - answer: \(answer ?? "nil")")
77
+ resolve(answer)
78
+ }
79
+
80
+ @objc
81
+ func clearPendingCallKitAnswer(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
82
+ UserDefaults.standard.removeObject(forKey: "pending_callkit_answer")
83
+ UserDefaults.standard.synchronize()
84
+ NSLog("[VoicePnBridge] clearPendingCallKitAnswer - cleared")
85
+ resolve(true)
86
+ }
87
+
73
88
  @objc
74
89
  func getPendingVoipAction(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
75
90
  let pending = UserDefaults.standard.string(forKey: "pending_voip_action")
@@ -258,6 +258,10 @@ class CallKitCoordinator {
258
258
  console.log('CallKitCoordinator: Answer action already being processed, skipping duplicate');
259
259
  return;
260
260
  }
261
+ // Clear native-side pending answer since JS received the event normally
262
+ try {
263
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
264
+ } catch (_) {}
261
265
  const call = this.callMap.get(callKitUUID);
262
266
  if (!call) {
263
267
  console.warn('CallKitCoordinator: No WebRTC call found for CallKit answer action', {
@@ -440,6 +444,21 @@ class CallKitCoordinator {
440
444
  ...realPushData.metadata,
441
445
  from_callkit: true,
442
446
  };
447
+ // Check if the user answered from CallKit before JS listeners were ready.
448
+ // The native CXAnswerCallAction handler persists the answer UUID in UserDefaults
449
+ // so we can detect it here even when the JS event was dropped.
450
+ try {
451
+ const pendingAnswer = await voice_pn_bridge_1.VoicePnBridge.getPendingCallKitAnswer();
452
+ if (pendingAnswer) {
453
+ console.log(
454
+ 'CallKitCoordinator: Found pending CallKit answer from native (JS event was missed), setting auto-answer flag'
455
+ );
456
+ this.shouldAutoAnswerNextCall = true;
457
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
458
+ }
459
+ } catch (e) {
460
+ // Ignore - method may not exist on older native versions
461
+ }
443
462
  // Check if auto-answer is set and add from_notification flag
444
463
  const shouldAddFromNotification = this.shouldAutoAnswerNextCall;
445
464
  let pushData;
@@ -717,6 +736,10 @@ class CallKitCoordinator {
717
736
  async cleanupPushNotificationState() {
718
737
  console.log('CallKitCoordinator: ✅ Cleared auto-answer flag');
719
738
  this.shouldAutoAnswerNextCall = false;
739
+ // Also clear native-side pending answer to prevent stale auto-answer
740
+ try {
741
+ await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
742
+ } catch (_) {}
720
743
  }
721
744
  /**
722
745
  * Get reference to the SDK client (for queuing actions when call doesn't exist yet)
@@ -25,6 +25,8 @@ export interface VoicePnBridgeInterface {
25
25
  ): Promise<boolean>;
26
26
  hideOngoingCallNotification(): Promise<boolean>;
27
27
  hideIncomingCallNotification(): Promise<boolean>;
28
+ getPendingCallKitAnswer(): Promise<string | null>;
29
+ clearPendingCallKitAnswer(): Promise<boolean>;
28
30
  getVoipToken(): Promise<string | null>;
29
31
  getPendingVoipPush(): Promise<string | null>;
30
32
  clearPendingVoipPush(): Promise<boolean>;
@@ -87,6 +89,16 @@ export declare class VoicePnBridge {
87
89
  * Useful for dismissing notifications when call is answered/rejected in app
88
90
  */
89
91
  static hideIncomingCallNotification(): Promise<boolean>;
92
+ /**
93
+ * Get pending CallKit answer UUID from native storage (iOS only).
94
+ * When the user answers a CallKit call before JS listeners are ready,
95
+ * the native side persists the answer UUID in UserDefaults so JS can detect it.
96
+ */
97
+ static getPendingCallKitAnswer(): Promise<string | null>;
98
+ /**
99
+ * Clear pending CallKit answer from native storage (iOS only)
100
+ */
101
+ static clearPendingCallKitAnswer(): Promise<boolean>;
90
102
  /**
91
103
  * Get VoIP token from native storage
92
104
  */
@@ -124,6 +124,32 @@ class VoicePnBridge {
124
124
  return false;
125
125
  }
126
126
  }
127
+ /**
128
+ * Get pending CallKit answer UUID from native storage (iOS only).
129
+ * When the user answers a CallKit call before JS listeners are ready,
130
+ * the native side persists the answer UUID in UserDefaults so JS can detect it.
131
+ */
132
+ static async getPendingCallKitAnswer() {
133
+ if (react_native_1.Platform.OS !== 'ios') return null;
134
+ try {
135
+ return await NativeBridge.getPendingCallKitAnswer();
136
+ } catch (error) {
137
+ console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Clear pending CallKit answer from native storage (iOS only)
143
+ */
144
+ static async clearPendingCallKitAnswer() {
145
+ if (react_native_1.Platform.OS !== 'ios') return true;
146
+ try {
147
+ return await NativeBridge.clearPendingCallKitAnswer();
148
+ } catch (error) {
149
+ console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
150
+ return false;
151
+ }
152
+ }
127
153
  /**
128
154
  * Get VoIP token from native storage
129
155
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -5,7 +5,6 @@ import { VoicePnBridge } from '../internal/voice-pn-bridge';
5
5
  import { router } from 'expo-router';
6
6
  import { TelnyxVoipClient } from '../telnyx-voip-client';
7
7
  import { TelnyxConnectionState } from '../models/connection-state';
8
- import { act } from 'react';
9
8
 
10
9
  /**
11
10
  * CallKit Coordinator - Manages the proper CallKit-first flow for iOS
@@ -254,6 +253,11 @@ class CallKitCoordinator {
254
253
  return;
255
254
  }
256
255
 
256
+ // Clear native-side pending answer since JS received the event normally
257
+ try {
258
+ await VoicePnBridge.clearPendingCallKitAnswer();
259
+ } catch (_) {}
260
+
257
261
  const call = this.callMap.get(callKitUUID);
258
262
 
259
263
  if (!call) {
@@ -469,6 +473,22 @@ class CallKitCoordinator {
469
473
  from_callkit: true,
470
474
  };
471
475
 
476
+ // Check if the user answered from CallKit before JS listeners were ready.
477
+ // The native CXAnswerCallAction handler persists the answer UUID in UserDefaults
478
+ // so we can detect it here even when the JS event was dropped.
479
+ try {
480
+ const pendingAnswer = await VoicePnBridge.getPendingCallKitAnswer();
481
+ if (pendingAnswer) {
482
+ console.log(
483
+ 'CallKitCoordinator: Found pending CallKit answer from native (JS event was missed), setting auto-answer flag'
484
+ );
485
+ this.shouldAutoAnswerNextCall = true;
486
+ await VoicePnBridge.clearPendingCallKitAnswer();
487
+ }
488
+ } catch (e) {
489
+ // Ignore - method may not exist on older native versions
490
+ }
491
+
472
492
  // Check if auto-answer is set and add from_notification flag
473
493
  const shouldAddFromNotification = this.shouldAutoAnswerNextCall;
474
494
 
@@ -789,6 +809,10 @@ class CallKitCoordinator {
789
809
  private async cleanupPushNotificationState(): Promise<void> {
790
810
  console.log('CallKitCoordinator: ✅ Cleared auto-answer flag');
791
811
  this.shouldAutoAnswerNextCall = false;
812
+ // Also clear native-side pending answer to prevent stale auto-answer
813
+ try {
814
+ await VoicePnBridge.clearPendingCallKitAnswer();
815
+ } catch (_) {}
792
816
  }
793
817
 
794
818
  /**
@@ -32,6 +32,10 @@ export interface VoicePnBridgeInterface {
32
32
  hideOngoingCallNotification(): Promise<boolean>;
33
33
  hideIncomingCallNotification(): Promise<boolean>;
34
34
 
35
+ // CallKit answer persistence (iOS only)
36
+ getPendingCallKitAnswer(): Promise<string | null>;
37
+ clearPendingCallKitAnswer(): Promise<boolean>;
38
+
35
39
  // Additional UserDefaults methods
36
40
  getVoipToken(): Promise<string | null>;
37
41
  getPendingVoipPush(): Promise<string | null>;
@@ -179,6 +183,34 @@ export class VoicePnBridge {
179
183
  }
180
184
  }
181
185
 
186
+ /**
187
+ * Get pending CallKit answer UUID from native storage (iOS only).
188
+ * When the user answers a CallKit call before JS listeners are ready,
189
+ * the native side persists the answer UUID in UserDefaults so JS can detect it.
190
+ */
191
+ static async getPendingCallKitAnswer(): Promise<string | null> {
192
+ if (Platform.OS !== 'ios') return null;
193
+ try {
194
+ return await NativeBridge.getPendingCallKitAnswer();
195
+ } catch (error) {
196
+ console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
197
+ return null;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Clear pending CallKit answer from native storage (iOS only)
203
+ */
204
+ static async clearPendingCallKitAnswer(): Promise<boolean> {
205
+ if (Platform.OS !== 'ios') return true;
206
+ try {
207
+ return await NativeBridge.clearPendingCallKitAnswer();
208
+ } catch (error) {
209
+ console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
210
+ return false;
211
+ }
212
+ }
213
+
182
214
  /**
183
215
  * Get VoIP token from native storage
184
216
  */