@telnyx/react-voice-commons-sdk 0.1.8-beta.0 → 0.1.8

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,36 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.1.8](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.8) (2026-03-09)
4
+
5
+ ### Bug Fixing
6
+
7
+ • Fixed duplicate CXProvider overwrite causing `CXEndCallAction` error 4 (unknownCallUUID) — guard `setupCallKit()` to prevent async `setupAutomatically()` from overwriting the provider created by `setupSynchronously()` during VoIP push handling
8
+ • Fixed intermittent audio loss on push notification calls — defer `CXAnswerCallAction.fulfill()` until `reportCallConnected()` when the WebRTC peer connection is ready
9
+ • Aligned iOS audio session handling with native Telnyx iOS SDK pattern (`RTCAudioSessionConfiguration.webRTC()` with `lockForConfiguration`/`unlockForConfiguration`)
10
+ • Fixed endCall not dismissing CallKit UI — check `endCall()` return value and fallback to `reportCallEnded()` when CXEndCallAction fails
11
+ • Fixed push notification answer crash when voipClient is not yet initialized — defer to `checkForInitialPushNotification()` instead of failing the call
12
+ • Added `voipClient.queueAnswerFromCallKit()` call when handling push notification answer for auto-answer on INVITE arrival
13
+ • Fixed call ENDED state not received by subscribers
14
+ • Added platform guards for iOS-only VoIP bridge methods on Android
15
+ • Fixed push data race condition in Expo apps
16
+
17
+ ## [0.1.8-beta.1](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.8-beta.1) (2026-03-04)
18
+
19
+ ### Bug Fixing
20
+
21
+ • Fixed duplicate CXProvider overwrite causing `CXEndCallAction` error 4 (unknownCallUUID) — guard `setupCallKit()` to prevent async `setupAutomatically()` from overwriting the provider created by `setupSynchronously()` during VoIP push handling
22
+ • Fixed intermittent audio loss on push notification calls — defer `CXAnswerCallAction.fulfill()` until `reportCallConnected()` when the WebRTC peer connection is ready
23
+ • Aligned iOS audio session handling with native Telnyx iOS SDK pattern (`RTCAudioSessionConfiguration.webRTC()` with `lockForConfiguration`/`unlockForConfiguration`)
24
+ • Fixed endCall not dismissing CallKit UI — check `endCall()` return value and fallback to `reportCallEnded()` when CXEndCallAction fails
25
+ • Fixed push notification answer crash when voipClient is not yet initialized — defer to `checkForInitialPushNotification()` instead of failing the call
26
+ • Added `voipClient.queueAnswerFromCallKit()` call when handling push notification answer for auto-answer on INVITE arrival
27
+
28
+ ## [0.1.8-beta.0](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.8-beta.0) (2026-02-28)
29
+
30
+ ### Bug Fixing
31
+
32
+ • Fixed push data race condition in Expo apps
33
+
3
34
  ## [0.1.7](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.7) (2026-02-20)
4
35
 
5
36
  ### Enhancement
@@ -205,6 +205,33 @@ import React
205
205
  }
206
206
 
207
207
  provider.reportOutgoingCall(with: uuid, connectedAt: Date())
208
+
209
+ // Fulfill deferred CXAnswerCallAction now that peer connection is ready
210
+ if let pendingAction = manager.pendingAnswerAction {
211
+ NSLog("TelnyxVoice: reportCallConnected - fulfilling deferred CXAnswerCallAction")
212
+ pendingAction.fulfill()
213
+ manager.pendingAnswerAction = nil
214
+ } else {
215
+ // Fallback: ensure audio is enabled for non-push or already-fulfilled cases
216
+ let rtcAudioSession = RTCAudioSession.sharedInstance()
217
+ rtcAudioSession.lockForConfiguration()
218
+ let webRTCConfig = RTCAudioSessionConfiguration.webRTC()
219
+ webRTCConfig.categoryOptions = [.duckOthers, .allowBluetooth]
220
+ do {
221
+ try rtcAudioSession.setConfiguration(webRTCConfig)
222
+ } catch {
223
+ NSLog("TelnyxVoice: reportCallConnected - setConfiguration error: \(error)")
224
+ }
225
+ do {
226
+ try rtcAudioSession.setActive(true)
227
+ } catch {
228
+ NSLog("TelnyxVoice: reportCallConnected - setActive error: \(error)")
229
+ }
230
+ rtcAudioSession.isAudioEnabled = true
231
+ rtcAudioSession.unlockForConfiguration()
232
+ rtcAudioSession.audioSessionDidActivate(AVAudioSession.sharedInstance())
233
+ }
234
+
208
235
  resolve(["success": true])
209
236
  }
210
237
 
@@ -276,6 +303,7 @@ import React
276
303
  public var callKitProvider: CXProvider?
277
304
  public var callKitController: CXCallController?
278
305
  public var activeCalls: [UUID: [String: Any]] = [:]
306
+ public var pendingAnswerAction: CXAnswerCallAction?
279
307
 
280
308
  private override init() {
281
309
  super.init()
@@ -327,12 +355,16 @@ import React
327
355
  }
328
356
 
329
357
  private func setupCallKit() {
330
- // CRITICAL: Configure WebRTC for manual audio control BEFORE CallKit setup
358
+ // ALWAYS configure WebRTC audio, even if provider already exists
331
359
  RTCAudioSession.sharedInstance().useManualAudio = true
332
- RTCAudioSession.sharedInstance().isAudioEnabled = false // MUST be false initially!
333
- NSLog(
334
- "🎧 TelnyxVoice: WebRTC configured for manual audio control (audio DISABLED until CallKit activates)"
335
- )
360
+ RTCAudioSession.sharedInstance().isAudioEnabled = false
361
+
362
+ // Guard against duplicate provider creation (async setupAutomatically can overwrite
363
+ // the provider created by setupSynchronously for VoIP push)
364
+ guard callKitProvider == nil else {
365
+ NSLog("TelnyxVoice: setupCallKit() - provider already exists, skipping to prevent overwrite")
366
+ return
367
+ }
336
368
 
337
369
  // Use the localizedName from the app's bundle display name or fallback
338
370
  let appName =
@@ -351,11 +383,7 @@ import React
351
383
  callKitProvider?.setDelegate(self, queue: nil)
352
384
  callKitController = CXCallController()
353
385
 
354
- NSLog(
355
- "📞 TelnyxVoice: CallKit provider instance: \(String(describing: callKitProvider))")
356
- NSLog(
357
- "📞 TelnyxVoice: CallKit controller instance: \(String(describing: callKitController))"
358
- )
386
+ NSLog("TelnyxVoice: CallKit provider and controller created")
359
387
  }
360
388
 
361
389
 
@@ -519,9 +547,9 @@ import React
519
547
  callData: activeCalls[action.callUUID])
520
548
  }
521
549
 
522
- NSLog("📞 TelnyxVoice: Fulfilling CXAnswerCallAction for call UUID: \(action.callUUID)")
523
- action.fulfill()
524
- NSLog("📞 TelnyxVoice: ✅ CXAnswerCallAction fulfilled successfully")
550
+ // Defer action.fulfill() until reportCallConnected when peer connection is ready
551
+ NSLog("TelnyxVoice: Deferring CXAnswerCallAction.fulfill() until peer connection is ready")
552
+ self.pendingAnswerAction = action
525
553
  }
526
554
 
527
555
  public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
@@ -552,58 +580,67 @@ import React
552
580
  }
553
581
 
554
582
  public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
555
- NSLog(
556
- "🎧🎧🎧 TelnyxVoice: AUDIO SESSION ACTIVATED BY CALLKIT - USER ANSWERED THE CALL! 🎧🎧🎧")
557
- NSLog("🎧 Provider: \(provider)")
583
+ NSLog("TelnyxVoice: Audio session activated by CallKit")
558
584
 
585
+ let rtcAudioSession = RTCAudioSession.sharedInstance()
559
586
 
560
- // CRITICAL: Activate WebRTC audio session
561
- RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
562
- RTCAudioSession.sharedInstance().isAudioEnabled = true
563
- NSLog("🎧 TelnyxVoice: WebRTC RTCAudioSession activated and audio enabled")
564
-
587
+ // Step 1: Configure (matches native iOS SDK setupCorrectAudioConfiguration)
588
+ rtcAudioSession.lockForConfiguration()
589
+ let webRTCConfig = RTCAudioSessionConfiguration.webRTC()
590
+ webRTCConfig.categoryOptions = [.duckOthers, .allowBluetooth]
565
591
  do {
566
- // Configure audio session for VoIP with proper routing
567
- try audioSession.setCategory(
568
- .playAndRecord, mode: .voiceChat,
569
- options: [.allowBluetooth, .allowBluetoothA2DP])
570
- try audioSession.setActive(true)
571
-
572
- // Emit audio session activated event to React Native
573
- CallKitBridge.shared?.emitAudioSessionEvent(
574
- "AudioSessionActivated",
575
- data: [
576
- "category": audioSession.category.rawValue,
577
- "mode": audioSession.mode.rawValue,
578
- "isActive": true,
579
- ])
580
-
581
- NSLog(
582
- "🎧 SUCCESS: Audio session ACTIVE for VoIP - Category: \(audioSession.category.rawValue), Mode: \(audioSession.mode.rawValue)"
583
- )
592
+ try rtcAudioSession.setConfiguration(webRTCConfig)
584
593
  } catch {
585
- NSLog("❌ FAILED: Audio session configuration error: \(error)")
594
+ NSLog("TelnyxVoice: didActivateAudioSession - setConfiguration error: \(error)")
595
+ }
596
+ rtcAudioSession.unlockForConfiguration()
586
597
 
587
- // Emit audio session failed event to React Native
588
- CallKitBridge.shared?.emitAudioSessionEvent(
589
- "AudioSessionFailed",
590
- data: [
591
- "error": error.localizedDescription
592
- ])
598
+ // Step 2: Activate (matches native iOS SDK setAudioSessionActive)
599
+ rtcAudioSession.lockForConfiguration()
600
+ do {
601
+ try rtcAudioSession.setActive(true)
602
+ } catch {
603
+ NSLog("TelnyxVoice: didActivateAudioSession - setActive error: \(error)")
593
604
  }
605
+ rtcAudioSession.isAudioEnabled = true
606
+ rtcAudioSession.unlockForConfiguration()
607
+
608
+ rtcAudioSession.audioSessionDidActivate(audioSession)
609
+
610
+ // Emit audio session activated event to React Native
611
+ CallKitBridge.shared?.emitAudioSessionEvent(
612
+ "AudioSessionActivated",
613
+ data: [
614
+ "category": audioSession.category.rawValue,
615
+ "mode": audioSession.mode.rawValue,
616
+ "isActive": true,
617
+ ])
594
618
  }
595
619
 
596
620
  public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
597
- NSLog("🔇🔇🔇 TelnyxVoice: AUDIO SESSION DEACTIVATED BY CALLKIT 🔇🔇🔇")
598
- NSLog("🔇 Provider: \(provider)")
599
- NSLog(
600
- "🔇 Audio session details: active=\(audioSession.isOtherAudioPlaying), category=\(audioSession.category.rawValue), mode=\(audioSession.mode.rawValue)"
601
- )
621
+ NSLog("TelnyxVoice: Audio session deactivated by CallKit")
602
622
 
603
- // CRITICAL: Deactivate WebRTC audio session (matches Flutter implementation)
604
- RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
605
- RTCAudioSession.sharedInstance().isAudioEnabled = false
606
- NSLog("🔇 TelnyxVoice: WebRTC RTCAudioSession deactivated and audio disabled")
623
+ let rtcAudioSession = RTCAudioSession.sharedInstance()
624
+
625
+ // Reset audio config (matches native iOS SDK resetAudioConfiguration)
626
+ let avAudioSession = AVAudioSession.sharedInstance()
627
+ do {
628
+ try avAudioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
629
+ } catch {
630
+ NSLog("TelnyxVoice: didDeactivateAudioSession - setCategory error: \(error)")
631
+ }
632
+
633
+ // Deactivate (matches native iOS SDK setAudioSessionActive(false))
634
+ rtcAudioSession.lockForConfiguration()
635
+ do {
636
+ try rtcAudioSession.setActive(false)
637
+ } catch {
638
+ NSLog("TelnyxVoice: didDeactivateAudioSession - setActive(false) error: \(error)")
639
+ }
640
+ rtcAudioSession.isAudioEnabled = false
641
+ rtcAudioSession.unlockForConfiguration()
642
+
643
+ rtcAudioSession.audioSessionDidDeactivate(audioSession)
607
644
 
608
645
  // Emit audio session deactivated event to React Native
609
646
  CallKitBridge.shared?.emitAudioSessionEvent(
@@ -228,21 +228,26 @@ class CallKitCoordinator {
228
228
  'CallKitCoordinator: Ending call from UI - dismissing CallKit and hanging up WebRTC call',
229
229
  callKitUUID
230
230
  );
231
- // Mark as processing to prevent duplicate actions
232
- this.processingCalls.add(callKitUUID);
231
+ // Track this call as ended to prevent duplicate end actions
232
+ this.endedCalls.add(callKitUUID);
233
233
  try {
234
- // End the call in CallKit and hang up the WebRTC call
235
- await callkit_1.default.endCall(callKitUUID);
234
+ // End the call in CallKit - endCall returns false on failure (doesn't throw)
235
+ const endCallSuccess = await callkit_1.default.endCall(callKitUUID);
236
+ if (!endCallSuccess) {
237
+ // Fallback: use reportCallEnded to dismiss CallKit UI when endCall fails
238
+ // (e.g., unknownCallUUID error from duplicate CXProvider)
239
+ await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.RemoteEnded);
240
+ }
241
+ this.isCallFromPush = false;
236
242
  call.hangup();
237
- // Clean up the mappings
238
243
  this.cleanupCall(callKitUUID);
239
244
  return true;
240
245
  } catch (error) {
241
246
  console.error('CallKitCoordinator: Error ending call from UI', error);
242
- call.hangup(); // Ensure WebRTC call is ended
247
+ this.isCallFromPush = false;
248
+ call.hangup();
249
+ this.cleanupCall(callKitUUID);
243
250
  return false;
244
- } finally {
245
- this.processingCalls.delete(callKitUUID);
246
251
  }
247
252
  }
248
253
  /**
@@ -472,18 +477,22 @@ class CallKitCoordinator {
472
477
  // Set auto-answer flag so when the WebRTC call comes in, it will be answered automatically
473
478
  this.shouldAutoAnswerNextCall = true;
474
479
  console.log('CallKitCoordinator: ✅ Set auto-answer flag for next incoming call');
475
- // Get VoIP client and trigger reconnection
480
+ // Try to get VoIP client - it may not be wired yet if user answered
481
+ // from CallKit before React Native finished initializing
476
482
  const voipClient = this.getSDKClient();
477
483
  if (!voipClient) {
478
- console.error(
479
- 'CallKitCoordinator: No VoIP client available - cannot reconnect for push notification'
480
- );
481
- await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
482
- this.cleanupCall(callKitUUID);
484
+ // voipClient not ready yet - DON'T fail the call.
485
+ // shouldAutoAnswerNextCall is already set to true above.
486
+ // checkForInitialPushNotification() will run after setVoipClient()
487
+ // and will find the push data still intact, call handleCallKitPushReceived()
488
+ // which checks shouldAutoAnswerNextCall and queues the auto-answer.
483
489
  return;
484
490
  }
491
+ // voipClient is available - queue the answer action on the TelnyxRTC client
492
+ // so when the INVITE arrives after WebSocket login, processInvite() sees
493
+ // pendingAnswerAction=true and auto-answers the call.
494
+ voipClient.queueAnswerFromCallKit();
485
495
  // Get the real push data that was stored by the VoIP push handler
486
- console.log('CallKitCoordinator: 🔍 Getting real push data from VoicePnBridge...');
487
496
  let realPushData = null;
488
497
  try {
489
498
  const pendingPushJson = await voice_pn_bridge_1.VoicePnBridge.getPendingVoipPush();
@@ -587,16 +596,11 @@ class CallKitCoordinator {
587
596
  if (!this.connectedCalls.has(callKitUUID)) {
588
597
  console.log('CallKitCoordinator: WebRTC call active - reporting connected to CallKit');
589
598
  try {
590
- // Report as connected (CallKit call already answered in UI flow)
591
599
  await callkit_1.default.reportCallConnected(callKitUUID);
592
- console.log(
593
- 'CallKitCoordinator: Call reported as connected to CallKit ',
594
- callKitUUID
595
- );
596
- this.connectedCalls.add(callKitUUID);
597
600
  } catch (error) {
598
601
  console.error('CallKitCoordinator: Error reporting call connected:', error);
599
602
  }
603
+ this.connectedCalls.add(callKitUUID);
600
604
  }
601
605
  break;
602
606
  case 'ended':
@@ -343,12 +343,13 @@ class CallStateController {
343
343
  call.callState$.subscribe((state) => {
344
344
  // CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
345
345
  console.log('CallStateController: Call state changed to:', state);
346
- // Clean up when call ends
346
+ // Clean up when call ends - delay to next tick so external subscribers
347
+ // receive the ENDED/FAILED state before the call is disposed
347
348
  if (
348
349
  state === call_state_1.TelnyxCallState.ENDED ||
349
350
  state === call_state_1.TelnyxCallState.FAILED
350
351
  ) {
351
- this._removeCall(call.callId);
352
+ setTimeout(() => this._removeCall(call.callId), 0);
352
353
  }
353
354
  });
354
355
  }
@@ -92,19 +92,19 @@ export declare class VoicePnBridge {
92
92
  */
93
93
  static getVoipToken(): Promise<string | null>;
94
94
  /**
95
- * Get pending VoIP push from native storage
95
+ * Get pending VoIP push from native storage (iOS only)
96
96
  */
97
97
  static getPendingVoipPush(): Promise<string | null>;
98
98
  /**
99
- * Clear pending VoIP push from native storage
99
+ * Clear pending VoIP push from native storage (iOS only)
100
100
  */
101
101
  static clearPendingVoipPush(): Promise<boolean>;
102
102
  /**
103
- * Get pending VoIP action from native storage
103
+ * Get pending VoIP action from native storage (iOS only)
104
104
  */
105
105
  static getPendingVoipAction(): Promise<string | null>;
106
106
  /**
107
- * Clear pending VoIP action from native storage
107
+ * Clear pending VoIP action from native storage (iOS only)
108
108
  */
109
109
  static clearPendingVoipAction(): Promise<boolean>;
110
110
  /**
@@ -128,6 +128,7 @@ class VoicePnBridge {
128
128
  * Get VoIP token from native storage
129
129
  */
130
130
  static async getVoipToken() {
131
+ if (react_native_1.Platform.OS !== 'ios') return null;
131
132
  try {
132
133
  return await NativeBridge.getVoipToken();
133
134
  } catch (error) {
@@ -136,9 +137,10 @@ class VoicePnBridge {
136
137
  }
137
138
  }
138
139
  /**
139
- * Get pending VoIP push from native storage
140
+ * Get pending VoIP push from native storage (iOS only)
140
141
  */
141
142
  static async getPendingVoipPush() {
143
+ if (react_native_1.Platform.OS !== 'ios') return null;
142
144
  try {
143
145
  return await NativeBridge.getPendingVoipPush();
144
146
  } catch (error) {
@@ -147,9 +149,10 @@ class VoicePnBridge {
147
149
  }
148
150
  }
149
151
  /**
150
- * Clear pending VoIP push from native storage
152
+ * Clear pending VoIP push from native storage (iOS only)
151
153
  */
152
154
  static async clearPendingVoipPush() {
155
+ if (react_native_1.Platform.OS !== 'ios') return true;
153
156
  try {
154
157
  return await NativeBridge.clearPendingVoipPush();
155
158
  } catch (error) {
@@ -158,9 +161,10 @@ class VoicePnBridge {
158
161
  }
159
162
  }
160
163
  /**
161
- * Get pending VoIP action from native storage
164
+ * Get pending VoIP action from native storage (iOS only)
162
165
  */
163
166
  static async getPendingVoipAction() {
167
+ if (react_native_1.Platform.OS !== 'ios') return null;
164
168
  try {
165
169
  return await NativeBridge.getPendingVoipAction();
166
170
  } catch (error) {
@@ -169,9 +173,10 @@ class VoicePnBridge {
169
173
  }
170
174
  }
171
175
  /**
172
- * Clear pending VoIP action from native storage
176
+ * Clear pending VoIP action from native storage (iOS only)
173
177
  */
174
178
  static async clearPendingVoipAction() {
179
+ if (react_native_1.Platform.OS !== 'ios') return true;
175
180
  try {
176
181
  return await NativeBridge.clearPendingVoipAction();
177
182
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "0.1.8-beta.0",
3
+ "version": "0.1.8",
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",
@@ -218,24 +218,30 @@ class CallKitCoordinator {
218
218
  callKitUUID
219
219
  );
220
220
 
221
- // Mark as processing to prevent duplicate actions
222
- this.processingCalls.add(callKitUUID);
221
+ // Track this call as ended to prevent duplicate end actions
222
+ this.endedCalls.add(callKitUUID);
223
223
 
224
224
  try {
225
- // End the call in CallKit and hang up the WebRTC call
226
- await CallKit.endCall(callKitUUID);
227
- call.hangup();
225
+ // End the call in CallKit - endCall returns false on failure (doesn't throw)
226
+ const endCallSuccess = await CallKit.endCall(callKitUUID);
227
+
228
+ if (!endCallSuccess) {
229
+ // Fallback: use reportCallEnded to dismiss CallKit UI when endCall fails
230
+ // (e.g., unknownCallUUID error from duplicate CXProvider)
231
+ await CallKit.reportCallEnded(callKitUUID, CallEndReason.RemoteEnded);
232
+ }
228
233
 
229
- // Clean up the mappings
234
+ this.isCallFromPush = false;
235
+ call.hangup();
230
236
  this.cleanupCall(callKitUUID);
231
237
 
232
238
  return true;
233
239
  } catch (error) {
234
240
  console.error('CallKitCoordinator: Error ending call from UI', error);
235
- call.hangup(); // Ensure WebRTC call is ended
241
+ this.isCallFromPush = false;
242
+ call.hangup();
243
+ this.cleanupCall(callKitUUID);
236
244
  return false;
237
- } finally {
238
- this.processingCalls.delete(callKitUUID);
239
245
  }
240
246
  }
241
247
 
@@ -506,19 +512,24 @@ class CallKitCoordinator {
506
512
  this.shouldAutoAnswerNextCall = true;
507
513
  console.log('CallKitCoordinator: ✅ Set auto-answer flag for next incoming call');
508
514
 
509
- // Get VoIP client and trigger reconnection
515
+ // Try to get VoIP client - it may not be wired yet if user answered
516
+ // from CallKit before React Native finished initializing
510
517
  const voipClient = this.getSDKClient();
511
518
  if (!voipClient) {
512
- console.error(
513
- 'CallKitCoordinator: No VoIP client available - cannot reconnect for push notification'
514
- );
515
- await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
516
- this.cleanupCall(callKitUUID);
519
+ // voipClient not ready yet - DON'T fail the call.
520
+ // shouldAutoAnswerNextCall is already set to true above.
521
+ // checkForInitialPushNotification() will run after setVoipClient()
522
+ // and will find the push data still intact, call handleCallKitPushReceived()
523
+ // which checks shouldAutoAnswerNextCall and queues the auto-answer.
517
524
  return;
518
525
  }
519
526
 
527
+ // voipClient is available - queue the answer action on the TelnyxRTC client
528
+ // so when the INVITE arrives after WebSocket login, processInvite() sees
529
+ // pendingAnswerAction=true and auto-answers the call.
530
+ voipClient.queueAnswerFromCallKit();
531
+
520
532
  // Get the real push data that was stored by the VoIP push handler
521
- console.log('CallKitCoordinator: 🔍 Getting real push data from VoicePnBridge...');
522
533
  let realPushData = null;
523
534
  try {
524
535
  const pendingPushJson = await VoicePnBridge.getPendingVoipPush();
@@ -638,17 +649,11 @@ class CallKitCoordinator {
638
649
  console.log('CallKitCoordinator: WebRTC call active - reporting connected to CallKit');
639
650
 
640
651
  try {
641
- // Report as connected (CallKit call already answered in UI flow)
642
652
  await CallKit.reportCallConnected(callKitUUID);
643
- console.log(
644
- 'CallKitCoordinator: Call reported as connected to CallKit ',
645
- callKitUUID
646
- );
647
-
648
- this.connectedCalls.add(callKitUUID);
649
653
  } catch (error) {
650
654
  console.error('CallKitCoordinator: Error reporting call connected:', error);
651
655
  }
656
+ this.connectedCalls.add(callKitUUID);
652
657
  }
653
658
  break;
654
659
 
@@ -405,9 +405,10 @@ export class CallStateController {
405
405
  // CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
406
406
  console.log('CallStateController: Call state changed to:', state);
407
407
 
408
- // Clean up when call ends
408
+ // Clean up when call ends - delay to next tick so external subscribers
409
+ // receive the ENDED/FAILED state before the call is disposed
409
410
  if (state === TelnyxCallState.ENDED || state === TelnyxCallState.FAILED) {
410
- this._removeCall(call.callId);
411
+ setTimeout(() => this._removeCall(call.callId), 0);
411
412
  }
412
413
  });
413
414
  }
@@ -1,4 +1,4 @@
1
- import { NativeModules, DeviceEventEmitter, EmitterSubscription } from 'react-native';
1
+ import { NativeModules, DeviceEventEmitter, EmitterSubscription, Platform } from 'react-native';
2
2
 
3
3
  export interface CallActionEvent {
4
4
  action: string;
@@ -183,6 +183,7 @@ export class VoicePnBridge {
183
183
  * Get VoIP token from native storage
184
184
  */
185
185
  static async getVoipToken(): Promise<string | null> {
186
+ if (Platform.OS !== 'ios') return null;
186
187
  try {
187
188
  return await NativeBridge.getVoipToken();
188
189
  } catch (error) {
@@ -192,9 +193,10 @@ export class VoicePnBridge {
192
193
  }
193
194
 
194
195
  /**
195
- * Get pending VoIP push from native storage
196
+ * Get pending VoIP push from native storage (iOS only)
196
197
  */
197
198
  static async getPendingVoipPush(): Promise<string | null> {
199
+ if (Platform.OS !== 'ios') return null;
198
200
  try {
199
201
  return await NativeBridge.getPendingVoipPush();
200
202
  } catch (error) {
@@ -204,9 +206,10 @@ export class VoicePnBridge {
204
206
  }
205
207
 
206
208
  /**
207
- * Clear pending VoIP push from native storage
209
+ * Clear pending VoIP push from native storage (iOS only)
208
210
  */
209
211
  static async clearPendingVoipPush(): Promise<boolean> {
212
+ if (Platform.OS !== 'ios') return true;
210
213
  try {
211
214
  return await NativeBridge.clearPendingVoipPush();
212
215
  } catch (error) {
@@ -216,9 +219,10 @@ export class VoicePnBridge {
216
219
  }
217
220
 
218
221
  /**
219
- * Get pending VoIP action from native storage
222
+ * Get pending VoIP action from native storage (iOS only)
220
223
  */
221
224
  static async getPendingVoipAction(): Promise<string | null> {
225
+ if (Platform.OS !== 'ios') return null;
222
226
  try {
223
227
  return await NativeBridge.getPendingVoipAction();
224
228
  } catch (error) {
@@ -228,9 +232,10 @@ export class VoicePnBridge {
228
232
  }
229
233
 
230
234
  /**
231
- * Clear pending VoIP action from native storage
235
+ * Clear pending VoIP action from native storage (iOS only)
232
236
  */
233
237
  static async clearPendingVoipAction(): Promise<boolean> {
238
+ if (Platform.OS !== 'ios') return true;
234
239
  try {
235
240
  return await NativeBridge.clearPendingVoipAction();
236
241
  } catch (error) {
@@ -437,7 +437,9 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
437
437
  voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED ||
438
438
  voipClient.currentConnectionState === TelnyxConnectionState.CONNECTING
439
439
  ) {
440
- log(`SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`);
440
+ log(
441
+ `SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
442
+ );
441
443
  return;
442
444
  }
443
445