@telnyx/react-voice-commons-sdk 0.1.8-beta.0 → 0.1.8-beta.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,16 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.1.8-beta.1](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.8-beta.1) (2026-03-04)
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
+
3
14
  ## [0.1.7](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/0.1.7) (2026-02-20)
4
15
 
5
16
  ### 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':
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-beta.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",
@@ -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