@telnyx/react-voice-commons-sdk 1.2.0 → 1.2.2-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,18 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [1.2.2-beta.1] (2026-11-03)
4
+
5
+ ### Dependencies
6
+
7
+ - Publish against `@telnyx/react-native-voice-sdk ^1.2.1-beta.1` for beta validation of the iOS signaling reconnect and call-reattach fixes. This release contains no Commons runtime code changes.
8
+
9
+ ## [1.2.1] (2026-10-30)
10
+
11
+ ### Bug Fixing
12
+
13
+ - 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.
14
+ - `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.
15
+
3
16
  ## [1.2.0] (2026-10-28)
4
17
 
5
18
  ### 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
- this.activeOrAnsweringCallUUID = null;
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
- const queued = await this.handlePushNotificationAnswer(callKitUUID, event);
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
@@ -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.0",
3
+ "version": "1.2.2-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",
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "@react-native-community/eslint-config": "^3.2.0",
89
- "@telnyx/react-native-voice-sdk": "^1.2.0",
89
+ "@telnyx/react-native-voice-sdk": "^1.2.1-beta.1",
90
90
  "eventemitter3": "^5.0.1",
91
91
  "react-native-url-polyfill": "^3.0.0",
92
92
  "react-native-voip-push-notification": "^3.3.3",
@@ -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
- private activeOrAnsweringCallUUID: string | null = null;
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
- const queued = await this.handlePushNotificationAnswer(callKitUUID, event);
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
@@ -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
  }