@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 +31 -0
- package/ios/CallKitBridge.swift +92 -55
- package/lib/callkit/callkit-coordinator.js +25 -21
- package/lib/internal/calls/call-state-controller.js +3 -2
- package/lib/internal/voice-pn-bridge.d.ts +4 -4
- package/lib/internal/voice-pn-bridge.js +9 -4
- package/package.json +1 -1
- package/src/callkit/callkit-coordinator.ts +28 -23
- package/src/internal/calls/call-state-controller.ts +3 -2
- package/src/internal/voice-pn-bridge.ts +10 -5
- package/src/telnyx-voice-app.tsx +3 -1
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
|
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':
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
|
@@ -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) {
|
package/src/telnyx-voice-app.tsx
CHANGED
|
@@ -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(
|
|
440
|
+
log(
|
|
441
|
+
`SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
|
|
442
|
+
);
|
|
441
443
|
return;
|
|
442
444
|
}
|
|
443
445
|
|