@telnyx/react-voice-commons-sdk 1.2.0 → 1.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
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG.md
|
|
2
2
|
|
|
3
|
+
## [1.2.1] (2026-10-30)
|
|
4
|
+
|
|
5
|
+
### Bug Fixing
|
|
6
|
+
|
|
7
|
+
- Fix iOS call waiting. In 1.2.0, answering a second call failed while another call was active or still being answered, so the iOS Hold & Accept and End & Accept options did not connect the new call. Answering the same call twice is still ignored.
|
|
8
|
+
- `Call.answer()` on iOS now rejects when CallKit fails to answer the call, as `hold()` and `resume()` already do. It previously resolved even though the call was not answered, so apps that await `answer()` should handle the rejection.
|
|
9
|
+
|
|
3
10
|
## [1.2.0] (2026-10-28)
|
|
4
11
|
|
|
5
12
|
### Enhancement
|
|
@@ -18,8 +18,6 @@ declare class CallKitCoordinator {
|
|
|
18
18
|
private static instance;
|
|
19
19
|
private callMap;
|
|
20
20
|
private processingCalls;
|
|
21
|
-
private activeOrAnsweringCallUUID;
|
|
22
|
-
private claimActiveCall;
|
|
23
21
|
private endedCalls;
|
|
24
22
|
private connectedCalls;
|
|
25
23
|
private isCallFromPush;
|
|
@@ -144,7 +142,6 @@ declare class CallKitCoordinator {
|
|
|
144
142
|
private getSDKClient;
|
|
145
143
|
private normalizeUUID;
|
|
146
144
|
private actionKey;
|
|
147
|
-
private releaseActiveCall;
|
|
148
145
|
private selectCall;
|
|
149
146
|
private isSelectedCall;
|
|
150
147
|
private recordPendingSwapAction;
|
|
@@ -167,10 +164,6 @@ declare class CallKitCoordinator {
|
|
|
167
164
|
* This helps prevent premature flag resets during CallKit operations
|
|
168
165
|
*/
|
|
169
166
|
hasProcessingCalls(): boolean;
|
|
170
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
171
|
-
claimActiveCallForTesting(callKitUUID: string): boolean;
|
|
172
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
173
|
-
releaseActiveCallForTesting(callKitUUID: string): void;
|
|
174
167
|
/**
|
|
175
168
|
* Check if there's currently a call from push notification being processed
|
|
176
169
|
* This helps prevent disconnection during push call handling
|
|
@@ -64,13 +64,6 @@ const voice_pn_bridge_1 = require('../internal/voice-pn-bridge');
|
|
|
64
64
|
* guidelines for proper CallKit integration.
|
|
65
65
|
*/
|
|
66
66
|
class CallKitCoordinator {
|
|
67
|
-
claimActiveCall(callKitUUID) {
|
|
68
|
-
if (this.activeOrAnsweringCallUUID !== null && this.activeOrAnsweringCallUUID !== callKitUUID) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
this.activeOrAnsweringCallUUID = callKitUUID;
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
67
|
static getInstance() {
|
|
75
68
|
if (!CallKitCoordinator.instance) {
|
|
76
69
|
CallKitCoordinator.instance = new CallKitCoordinator();
|
|
@@ -83,7 +76,8 @@ class CallKitCoordinator {
|
|
|
83
76
|
// Tracks action+UUID pairs so an end action is not dropped merely because
|
|
84
77
|
// an answer or held action for the same call is still completing.
|
|
85
78
|
this.processingCalls = new Set();
|
|
86
|
-
|
|
79
|
+
// CallKit/the app owns cross-call hold and swap policy. Only serialize
|
|
80
|
+
// duplicate actions for the same UUID; held calls must not block a new answer.
|
|
87
81
|
// Tracks calls that have already been ended in CallKit to prevent duplicate reports
|
|
88
82
|
this.endedCalls = new Set();
|
|
89
83
|
// Tracks calls that have already been reported as connected to prevent duplicate reports
|
|
@@ -195,16 +189,6 @@ class CallKitCoordinator {
|
|
|
195
189
|
displayName || destinationNumber
|
|
196
190
|
);
|
|
197
191
|
if (success) {
|
|
198
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
199
|
-
console.warn('CallKitCoordinator: Another call is already active or answering');
|
|
200
|
-
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
201
|
-
try {
|
|
202
|
-
await call.hangup();
|
|
203
|
-
} finally {
|
|
204
|
-
this.cleanupCall(callKitUUID);
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
192
|
this.callMap.set(callKitUUID, call);
|
|
209
193
|
this.setupWebRTCCallListeners(call, callKitUUID);
|
|
210
194
|
call._callKitUUID = callKitUUID;
|
|
@@ -226,10 +210,6 @@ class CallKitCoordinator {
|
|
|
226
210
|
console.warn('CallKitCoordinator: Cannot answer call - no CallKit UUID found');
|
|
227
211
|
return false;
|
|
228
212
|
}
|
|
229
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
230
|
-
console.warn('CallKitCoordinator: Another call is already active or answering');
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
213
|
console.log(
|
|
234
214
|
'CallKitCoordinator: Answering call from UI using CallKit answer simulation',
|
|
235
215
|
callKitUUID
|
|
@@ -258,8 +238,6 @@ class CallKitCoordinator {
|
|
|
258
238
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
259
239
|
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
260
240
|
console.log('CallKitCoordinator: CallKit answer success');
|
|
261
|
-
} else {
|
|
262
|
-
this.releaseActiveCall(callKitUUID);
|
|
263
241
|
}
|
|
264
242
|
return success;
|
|
265
243
|
} catch (error) {
|
|
@@ -415,21 +393,6 @@ class CallKitCoordinator {
|
|
|
415
393
|
console.log('CallKitCoordinator: Answer action already being processed, skipping duplicate');
|
|
416
394
|
return;
|
|
417
395
|
}
|
|
418
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
419
|
-
console.warn(
|
|
420
|
-
'CallKitCoordinator: Rejecting answer because another call owns the active slot'
|
|
421
|
-
);
|
|
422
|
-
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
423
|
-
const rejectedCall = this.callMap.get(callKitUUID);
|
|
424
|
-
if (rejectedCall) {
|
|
425
|
-
try {
|
|
426
|
-
await rejectedCall.hangup();
|
|
427
|
-
} finally {
|
|
428
|
-
this.cleanupCall(callKitUUID);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
396
|
const call = this.callMap.get(callKitUUID);
|
|
434
397
|
if (!call) {
|
|
435
398
|
console.warn('CallKitCoordinator: No WebRTC call found for CallKit answer action', {
|
|
@@ -438,10 +401,7 @@ class CallKitCoordinator {
|
|
|
438
401
|
availableWebRTCCallIds: Array.from(this.callMap.values()).map((c) => c.callId),
|
|
439
402
|
});
|
|
440
403
|
console.log('CallKitCoordinator: No WebRTC call found, handling as push notification');
|
|
441
|
-
|
|
442
|
-
if (!queued) {
|
|
443
|
-
this.releaseActiveCall(callKitUUID);
|
|
444
|
-
}
|
|
404
|
+
await this.handlePushNotificationAnswer(callKitUUID, event);
|
|
445
405
|
return;
|
|
446
406
|
}
|
|
447
407
|
console.log('CallKitCoordinator: Processing CallKit answer action', {
|
|
@@ -832,18 +792,6 @@ class CallKitCoordinator {
|
|
|
832
792
|
});
|
|
833
793
|
switch (state) {
|
|
834
794
|
case 'active':
|
|
835
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
836
|
-
console.warn(
|
|
837
|
-
'CallKitCoordinator: Ending active call because another call owns the slot'
|
|
838
|
-
);
|
|
839
|
-
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
840
|
-
try {
|
|
841
|
-
await call.hangup();
|
|
842
|
-
} finally {
|
|
843
|
-
this.cleanupCall(callKitUUID);
|
|
844
|
-
}
|
|
845
|
-
break;
|
|
846
|
-
}
|
|
847
795
|
this.selectCall(call);
|
|
848
796
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
849
797
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
@@ -918,9 +866,6 @@ class CallKitCoordinator {
|
|
|
918
866
|
this.connectedCalls.delete(callKitUUID);
|
|
919
867
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
920
868
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
921
|
-
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
922
|
-
this.activeOrAnsweringCallUUID = null;
|
|
923
|
-
}
|
|
924
869
|
// Get the call before removing it
|
|
925
870
|
const call = this.callMap.get(callKitUUID);
|
|
926
871
|
// Clean up state listeners
|
|
@@ -1050,12 +995,6 @@ class CallKitCoordinator {
|
|
|
1050
995
|
actionKey(action, callKitUUID) {
|
|
1051
996
|
return `${action}:${this.normalizeUUID(callKitUUID)}`;
|
|
1052
997
|
}
|
|
1053
|
-
releaseActiveCall(callKitUUID) {
|
|
1054
|
-
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
1055
|
-
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
1056
|
-
this.activeOrAnsweringCallUUID = null;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
998
|
selectCall(call) {
|
|
1060
999
|
const callKitUUID = this.getCallKitUUID(call);
|
|
1061
1000
|
const wrapperCall = this.voipClient?.findCallByTelnyxCall(call);
|
|
@@ -1263,9 +1202,6 @@ class CallKitCoordinator {
|
|
|
1263
1202
|
resetFlags() {
|
|
1264
1203
|
console.log('CallKitCoordinator: Resetting coordinator flags');
|
|
1265
1204
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
1266
|
-
if (this.callMap.size === 0) {
|
|
1267
|
-
this.activeOrAnsweringCallUUID = null;
|
|
1268
|
-
}
|
|
1269
1205
|
console.log('CallKitCoordinator: ✅ Coordinator flags reset');
|
|
1270
1206
|
}
|
|
1271
1207
|
/**
|
|
@@ -1279,14 +1215,6 @@ class CallKitCoordinator {
|
|
|
1279
1215
|
// call arrives during push notification handling.
|
|
1280
1216
|
return this.processingCalls.size > 0 || this.isCallFromPush;
|
|
1281
1217
|
}
|
|
1282
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
1283
|
-
claimActiveCallForTesting(callKitUUID) {
|
|
1284
|
-
return this.claimActiveCall(callKitUUID);
|
|
1285
|
-
}
|
|
1286
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
1287
|
-
releaseActiveCallForTesting(callKitUUID) {
|
|
1288
|
-
this.releaseActiveCall(callKitUUID);
|
|
1289
|
-
}
|
|
1290
1218
|
/**
|
|
1291
1219
|
* Check if there's currently a call from push notification being processed
|
|
1292
1220
|
* This helps prevent disconnection during push call handling
|
package/lib/models/call.js
CHANGED
|
@@ -281,7 +281,10 @@ class Call {
|
|
|
281
281
|
);
|
|
282
282
|
if (callKitCoordinator.isAvailable()) {
|
|
283
283
|
console.log('Call: Using CallKit coordinator to answer call (iOS)');
|
|
284
|
-
await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
284
|
+
const answered = await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
285
|
+
if (!answered) {
|
|
286
|
+
throw new Error('CallKit failed to answer call');
|
|
287
|
+
}
|
|
285
288
|
return;
|
|
286
289
|
}
|
|
287
290
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.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",
|
|
@@ -45,15 +45,8 @@ class CallKitCoordinator {
|
|
|
45
45
|
// Tracks action+UUID pairs so an end action is not dropped merely because
|
|
46
46
|
// an answer or held action for the same call is still completing.
|
|
47
47
|
private processingCalls = new Set<string>();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
private claimActiveCall(callKitUUID: string): boolean {
|
|
51
|
-
if (this.activeOrAnsweringCallUUID !== null && this.activeOrAnsweringCallUUID !== callKitUUID) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
this.activeOrAnsweringCallUUID = callKitUUID;
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
48
|
+
// CallKit/the app owns cross-call hold and swap policy. Only serialize
|
|
49
|
+
// duplicate actions for the same UUID; held calls must not block a new answer.
|
|
57
50
|
|
|
58
51
|
// Tracks calls that have already been ended in CallKit to prevent duplicate reports
|
|
59
52
|
private endedCalls = new Set<string>();
|
|
@@ -201,16 +194,6 @@ class CallKitCoordinator {
|
|
|
201
194
|
);
|
|
202
195
|
|
|
203
196
|
if (success) {
|
|
204
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
205
|
-
console.warn('CallKitCoordinator: Another call is already active or answering');
|
|
206
|
-
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
207
|
-
try {
|
|
208
|
-
await call.hangup();
|
|
209
|
-
} finally {
|
|
210
|
-
this.cleanupCall(callKitUUID);
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
197
|
this.callMap.set(callKitUUID, call);
|
|
215
198
|
this.setupWebRTCCallListeners(call, callKitUUID);
|
|
216
199
|
(call as any)._callKitUUID = callKitUUID;
|
|
@@ -235,11 +218,6 @@ class CallKitCoordinator {
|
|
|
235
218
|
return false;
|
|
236
219
|
}
|
|
237
220
|
|
|
238
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
239
|
-
console.warn('CallKitCoordinator: Another call is already active or answering');
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
221
|
console.log(
|
|
244
222
|
'CallKitCoordinator: Answering call from UI using CallKit answer simulation',
|
|
245
223
|
callKitUUID
|
|
@@ -272,8 +250,6 @@ class CallKitCoordinator {
|
|
|
272
250
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
273
251
|
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
274
252
|
console.log('CallKitCoordinator: CallKit answer success');
|
|
275
|
-
} else {
|
|
276
|
-
this.releaseActiveCall(callKitUUID);
|
|
277
253
|
}
|
|
278
254
|
|
|
279
255
|
return success;
|
|
@@ -452,22 +428,6 @@ class CallKitCoordinator {
|
|
|
452
428
|
return;
|
|
453
429
|
}
|
|
454
430
|
|
|
455
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
456
|
-
console.warn(
|
|
457
|
-
'CallKitCoordinator: Rejecting answer because another call owns the active slot'
|
|
458
|
-
);
|
|
459
|
-
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
460
|
-
const rejectedCall = this.callMap.get(callKitUUID);
|
|
461
|
-
if (rejectedCall) {
|
|
462
|
-
try {
|
|
463
|
-
await rejectedCall.hangup();
|
|
464
|
-
} finally {
|
|
465
|
-
this.cleanupCall(callKitUUID);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
431
|
const call = this.callMap.get(callKitUUID);
|
|
472
432
|
|
|
473
433
|
if (!call) {
|
|
@@ -478,10 +438,7 @@ class CallKitCoordinator {
|
|
|
478
438
|
});
|
|
479
439
|
|
|
480
440
|
console.log('CallKitCoordinator: No WebRTC call found, handling as push notification');
|
|
481
|
-
|
|
482
|
-
if (!queued) {
|
|
483
|
-
this.releaseActiveCall(callKitUUID);
|
|
484
|
-
}
|
|
441
|
+
await this.handlePushNotificationAnswer(callKitUUID, event);
|
|
485
442
|
return;
|
|
486
443
|
}
|
|
487
444
|
|
|
@@ -935,18 +892,6 @@ class CallKitCoordinator {
|
|
|
935
892
|
|
|
936
893
|
switch (state) {
|
|
937
894
|
case 'active':
|
|
938
|
-
if (!this.claimActiveCall(callKitUUID)) {
|
|
939
|
-
console.warn(
|
|
940
|
-
'CallKitCoordinator: Ending active call because another call owns the slot'
|
|
941
|
-
);
|
|
942
|
-
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
943
|
-
try {
|
|
944
|
-
await call.hangup();
|
|
945
|
-
} finally {
|
|
946
|
-
this.cleanupCall(callKitUUID);
|
|
947
|
-
}
|
|
948
|
-
break;
|
|
949
|
-
}
|
|
950
895
|
this.selectCall(call);
|
|
951
896
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
952
897
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
@@ -1025,9 +970,6 @@ class CallKitCoordinator {
|
|
|
1025
970
|
this.connectedCalls.delete(callKitUUID);
|
|
1026
971
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
1027
972
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
1028
|
-
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
1029
|
-
this.activeOrAnsweringCallUUID = null;
|
|
1030
|
-
}
|
|
1031
973
|
|
|
1032
974
|
// Get the call before removing it
|
|
1033
975
|
const call = this.callMap.get(callKitUUID);
|
|
@@ -1185,13 +1127,6 @@ class CallKitCoordinator {
|
|
|
1185
1127
|
return `${action}:${this.normalizeUUID(callKitUUID)}`;
|
|
1186
1128
|
}
|
|
1187
1129
|
|
|
1188
|
-
private releaseActiveCall(callKitUUID: string): void {
|
|
1189
|
-
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
1190
|
-
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
1191
|
-
this.activeOrAnsweringCallUUID = null;
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
1130
|
private selectCall(call: Call): void {
|
|
1196
1131
|
const callKitUUID = this.getCallKitUUID(call);
|
|
1197
1132
|
const wrapperCall = this.voipClient?.findCallByTelnyxCall(call);
|
|
@@ -1430,10 +1365,6 @@ class CallKitCoordinator {
|
|
|
1430
1365
|
|
|
1431
1366
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
1432
1367
|
|
|
1433
|
-
if (this.callMap.size === 0) {
|
|
1434
|
-
this.activeOrAnsweringCallUUID = null;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
1368
|
console.log('CallKitCoordinator: ✅ Coordinator flags reset');
|
|
1438
1369
|
}
|
|
1439
1370
|
|
|
@@ -1449,16 +1380,6 @@ class CallKitCoordinator {
|
|
|
1449
1380
|
return this.processingCalls.size > 0 || this.isCallFromPush;
|
|
1450
1381
|
}
|
|
1451
1382
|
|
|
1452
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
1453
|
-
claimActiveCallForTesting(callKitUUID: string): boolean {
|
|
1454
|
-
return this.claimActiveCall(callKitUUID);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
/** @internal Exposed for deterministic single-active-call regression tests. */
|
|
1458
|
-
releaseActiveCallForTesting(callKitUUID: string): void {
|
|
1459
|
-
this.releaseActiveCall(callKitUUID);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
1383
|
/**
|
|
1463
1384
|
* Check if there's currently a call from push notification being processed
|
|
1464
1385
|
* This helps prevent disconnection during push call handling
|
package/src/models/call.ts
CHANGED
|
@@ -254,7 +254,10 @@ export class Call {
|
|
|
254
254
|
const { callKitCoordinator } = await import('../callkit/callkit-coordinator');
|
|
255
255
|
if (callKitCoordinator.isAvailable()) {
|
|
256
256
|
console.log('Call: Using CallKit coordinator to answer call (iOS)');
|
|
257
|
-
await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
257
|
+
const answered = await callKitCoordinator.answerCallFromUI(this._telnyxCall);
|
|
258
|
+
if (!answered) {
|
|
259
|
+
throw new Error('CallKit failed to answer call');
|
|
260
|
+
}
|
|
258
261
|
return;
|
|
259
262
|
}
|
|
260
263
|
}
|