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