@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 +11 -0
- package/ios/CallKitBridge.swift +92 -55
- package/lib/callkit/callkit-coordinator.js +25 -21
- package/package.json +1 -1
- package/src/callkit/callkit-coordinator.ts +28 -23
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
|
package/ios/CallKitBridge.swift
CHANGED
|
@@ -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
|
-
//
|
|
358
|
+
// ALWAYS configure WebRTC audio, even if provider already exists
|
|
331
359
|
RTCAudioSession.sharedInstance().useManualAudio = true
|
|
332
|
-
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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("
|
|
594
|
+
NSLog("TelnyxVoice: didActivateAudioSession - setConfiguration error: \(error)")
|
|
595
|
+
}
|
|
596
|
+
rtcAudioSession.unlockForConfiguration()
|
|
586
597
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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("
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
//
|
|
232
|
-
this.
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
|
|
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.
|
|
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
|
-
//
|
|
222
|
-
this.
|
|
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
|
|
226
|
-
await CallKit.endCall(callKitUUID);
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|