@telnyx/react-voice-commons-sdk 1.0.0 → 1.1.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -7
  3. package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
  4. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
  5. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +19 -0
  6. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +19 -0
  7. package/ios/CallKitBridge.m +20 -1
  8. package/ios/CallKitBridge.swift +527 -78
  9. package/ios/README.md +2 -2
  10. package/ios/VoicePnBridge.m +6 -0
  11. package/ios/VoicePnBridge.swift +15 -0
  12. package/lib/callkit/callkit-coordinator.d.ts +39 -3
  13. package/lib/callkit/callkit-coordinator.js +561 -148
  14. package/lib/callkit/callkit.d.ts +7 -0
  15. package/lib/callkit/callkit.js +83 -11
  16. package/lib/hooks/useAppStateHandler.js +6 -2
  17. package/lib/internal/CallKitHandler.d.ts +3 -0
  18. package/lib/internal/CallKitHandler.js +52 -46
  19. package/lib/internal/calls/call-state-controller.d.ts +33 -1
  20. package/lib/internal/calls/call-state-controller.js +100 -28
  21. package/lib/internal/session/session-manager.d.ts +15 -1
  22. package/lib/internal/session/session-manager.js +214 -62
  23. package/lib/internal/voice-pn-bridge.d.ts +18 -0
  24. package/lib/internal/voice-pn-bridge.js +39 -0
  25. package/lib/models/call.js +35 -0
  26. package/lib/models/config.d.ts +10 -0
  27. package/lib/models/config.js +2 -0
  28. package/lib/telnyx-voice-app.js +85 -43
  29. package/lib/telnyx-voip-client.d.ts +46 -5
  30. package/lib/telnyx-voip-client.js +238 -22
  31. package/package.json +6 -4
  32. package/src/callkit/callkit-coordinator.ts +642 -160
  33. package/src/callkit/callkit.ts +110 -12
  34. package/src/hooks/useAppStateHandler.ts +7 -2
  35. package/src/internal/CallKitHandler.tsx +60 -55
  36. package/src/internal/calls/call-state-controller.ts +112 -29
  37. package/src/internal/session/session-manager.ts +260 -65
  38. package/src/internal/voice-pn-bridge.ts +47 -0
  39. package/src/models/call.ts +34 -0
  40. package/src/models/config.ts +10 -0
  41. package/src/telnyx-voice-app.tsx +96 -45
  42. package/src/telnyx-voip-client.ts +216 -24
  43. package/src/types/telnyx-sdk.d.ts +2 -1
@@ -1,9 +1,62 @@
1
1
  'use strict';
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = {
10
+ enumerable: true,
11
+ get: function () {
12
+ return m[k];
13
+ },
14
+ };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }
18
+ : function (o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ });
22
+ var __setModuleDefault =
23
+ (this && this.__setModuleDefault) ||
24
+ (Object.create
25
+ ? function (o, v) {
26
+ Object.defineProperty(o, 'default', { enumerable: true, value: v });
27
+ }
28
+ : function (o, v) {
29
+ o['default'] = v;
30
+ });
31
+ var __importStar =
32
+ (this && this.__importStar) ||
33
+ (function () {
34
+ var ownKeys = function (o) {
35
+ ownKeys =
36
+ Object.getOwnPropertyNames ||
37
+ function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null)
48
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++)
49
+ if (k[i] !== 'default') __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
2
54
  Object.defineProperty(exports, '__esModule', { value: true });
3
55
  exports.TelnyxVoipClient = void 0;
4
56
  exports.createTelnyxVoipClient = createTelnyxVoipClient;
5
57
  exports.destroyTelnyxVoipClient = destroyTelnyxVoipClient;
6
58
  exports.createBackgroundTelnyxVoipClient = createBackgroundTelnyxVoipClient;
59
+ const react_native_1 = require('react-native');
7
60
  const connection_state_1 = require('./models/connection-state');
8
61
  const call_state_1 = require('./models/call-state');
9
62
  const config_1 = require('./models/config');
@@ -11,7 +64,9 @@ const session_manager_1 = require('./internal/session/session-manager');
11
64
  const call_state_controller_1 = require('./internal/calls/call-state-controller');
12
65
  const voice_pn_bridge_1 = require('./internal/voice-pn-bridge');
13
66
  const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
67
+ const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
14
68
  const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
69
+ const LEGACY_CALLKIT_ANSWER_KEY = '__legacy_callkit_answer__';
15
70
  /**
16
71
  * The main public interface for the react-voice-commons module.
17
72
  *
@@ -50,6 +105,7 @@ class TelnyxVoipClient {
50
105
  * @param options Configuration options for the client
51
106
  */
52
107
  constructor(options = {}) {
108
+ this._pendingCallKitAnswers = new Map();
53
109
  this._disposed = false;
54
110
  this._options = {
55
111
  enableAppStateManagement: true,
@@ -67,6 +123,7 @@ class TelnyxVoipClient {
67
123
  console.log(
68
124
  '🔧 TelnyxVoipClient: Client ready, initializing call state controller listeners'
69
125
  );
126
+ this._flushPendingCallKitAnswers();
70
127
  this._callStateController.initializeClientListeners();
71
128
  });
72
129
  // Clear any tracked calls when the session disconnects, so ghosts
@@ -157,6 +214,66 @@ class TelnyxVoipClient {
157
214
  getCall(callId) {
158
215
  return this._callStateController.getCall(callId);
159
216
  }
217
+ /**
218
+ * Explicitly set the active call for multi-call scenarios.
219
+ * @param callId The ID of the call to mark as active
220
+ */
221
+ setActiveCall(callId) {
222
+ this._throwIfDisposed();
223
+ this._callStateController.setActiveCall(callId);
224
+ }
225
+ /**
226
+ * Clear the explicitly selected active call and return to default selection.
227
+ */
228
+ clearActiveCall() {
229
+ this._throwIfDisposed();
230
+ this._callStateController.clearActiveCall();
231
+ }
232
+ /**
233
+ * Swap the current active call with a held call.
234
+ * On iOS this is coordinated through CallKit so native and SDK state stay aligned.
235
+ *
236
+ * @param targetCallId ID of the held call to make active
237
+ */
238
+ async swapCalls(targetCallId) {
239
+ this._throwIfDisposed();
240
+ const activeCall = this.currentActiveCall;
241
+ const heldCall = this.getCall(targetCallId);
242
+ if (!activeCall || !heldCall || activeCall.callId === heldCall.callId) {
243
+ throw new Error('An active call and a different held call are required to swap calls');
244
+ }
245
+ if (activeCall.currentState !== call_state_1.TelnyxCallState.ACTIVE) {
246
+ throw new Error(`Cannot swap active call in state: ${activeCall.currentState}`);
247
+ }
248
+ if (heldCall.currentState !== call_state_1.TelnyxCallState.HELD) {
249
+ throw new Error(`Cannot swap target call in state: ${heldCall.currentState}`);
250
+ }
251
+ if (react_native_1.Platform.OS === 'ios') {
252
+ const { callKitCoordinator } = await Promise.resolve().then(() =>
253
+ __importStar(require('./callkit/callkit-coordinator'))
254
+ );
255
+ if (callKitCoordinator.isAvailable()) {
256
+ const success = await callKitCoordinator.swapCallsFromUI(
257
+ activeCall.telnyxCall,
258
+ heldCall.telnyxCall
259
+ );
260
+ if (!success) {
261
+ throw new Error('CallKit failed to swap calls');
262
+ }
263
+ return;
264
+ }
265
+ }
266
+ await activeCall.hold();
267
+ try {
268
+ await heldCall.resume();
269
+ this.setActiveCall(heldCall.callId);
270
+ } catch (error) {
271
+ await activeCall.resume().catch((resumeError) => {
272
+ console.error('Failed to restore active call after swap failure', resumeError);
273
+ });
274
+ throw error;
275
+ }
276
+ }
160
277
  /**
161
278
  * Current session ID (UUID) for this connection.
162
279
  */
@@ -188,10 +305,11 @@ class TelnyxVoipClient {
188
305
  if (this._options.debug) {
189
306
  console.log('TelnyxVoipClient: Logging in with credentials for user:', config.sipUser);
190
307
  }
191
- const loginConfig = {
308
+ const loginConfig = await this._withNativeVoipPushToken({
192
309
  ...config,
193
310
  useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
194
- };
311
+ });
312
+ await voice_pn_bridge_1.VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
195
313
  // Store credentials for future reconnection
196
314
  await this._storeCredentials(loginConfig);
197
315
  await this._sessionManager.connectWithCredential(loginConfig);
@@ -214,10 +332,11 @@ class TelnyxVoipClient {
214
332
  if (this._options.debug) {
215
333
  console.log('TelnyxVoipClient: Logging in with token');
216
334
  }
217
- const loginConfig = {
335
+ const loginConfig = await this._withNativeVoipPushToken({
218
336
  ...config,
219
337
  useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
220
- };
338
+ });
339
+ await voice_pn_bridge_1.VoicePnBridge.setIncomingCallRingtone(loginConfig.incomingCallRingtone);
221
340
  // Store token for future reconnection
222
341
  await this._storeToken(loginConfig);
223
342
  await this._sessionManager.connectWithToken(loginConfig);
@@ -258,19 +377,30 @@ class TelnyxVoipClient {
258
377
  const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
259
378
  const storedPushToken = await AsyncStorage.getItem('@push_token');
260
379
  const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
380
+ const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
261
381
  const storedMissedCallNotifications = await AsyncStorage.getItem(
262
382
  MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
263
383
  );
264
384
  const useTrickleIce =
265
385
  storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
386
+ const pushWhenActive = storedPushWhenActive === 'true';
266
387
  const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
388
+ const nativePushToken =
389
+ react_native_1.Platform.OS === 'ios'
390
+ ? (await voice_pn_bridge_1.VoicePnBridge.getVoipToken())?.trim()
391
+ : null;
392
+ const pushNotificationDeviceToken = nativePushToken || storedPushToken;
393
+ if (nativePushToken && nativePushToken !== storedPushToken) {
394
+ await AsyncStorage.setItem('@push_token', nativePushToken);
395
+ }
267
396
  // Check if we have credential-based authentication data
268
397
  if (storedUsername && storedPassword) {
269
398
  // Create credential config from stored data
270
399
  const { createCredentialConfig } = require('./models/config');
271
400
  const config = createCredentialConfig(storedUsername, storedPassword, {
272
- pushNotificationDeviceToken: storedPushToken,
401
+ pushNotificationDeviceToken,
273
402
  useTrickleIce,
403
+ pushWhenActive,
274
404
  enableMissedCallNotifications,
275
405
  });
276
406
  if (this._options.debug) {
@@ -287,8 +417,9 @@ class TelnyxVoipClient {
287
417
  // Create token config from stored data
288
418
  const { createTokenConfig } = require('./models/config');
289
419
  const config = createTokenConfig(storedCredentialToken, {
290
- pushNotificationDeviceToken: storedPushToken,
420
+ pushNotificationDeviceToken,
291
421
  useTrickleIce,
422
+ pushWhenActive,
292
423
  enableMissedCallNotifications,
293
424
  });
294
425
  if (this._options.debug) {
@@ -403,38 +534,60 @@ class TelnyxVoipClient {
403
534
  * This should be called when the user answers from CallKit before the socket connection is established
404
535
  * @param customHeaders Optional custom headers to include with the answer
405
536
  */
406
- queueAnswerFromCallKit(customHeaders = {}) {
537
+ queueAnswerFromCallKit(callKitUUIDOrHeaders, customHeaders = {}) {
407
538
  this._throwIfDisposed();
408
539
  if (this._options.debug) {
409
- console.log('TelnyxVoipClient: Queuing answer action from CallKit', customHeaders);
540
+ console.log('TelnyxVoipClient: Queuing answer action from CallKit', {
541
+ callKitUUIDOrHeaders,
542
+ customHeaders,
543
+ });
410
544
  }
411
545
  const telnyxClient = this._sessionManager.telnyxClient;
412
546
  if (telnyxClient && typeof telnyxClient.queueAnswerFromCallKit === 'function') {
413
- telnyxClient.queueAnswerFromCallKit(customHeaders);
547
+ telnyxClient.queueAnswerFromCallKit(callKitUUIDOrHeaders, customHeaders);
414
548
  } else {
415
- console.warn(
416
- 'TelnyxVoipClient: TelnyxRTC client not available or method not found for queueAnswerFromCallKit'
417
- );
549
+ const normalizedUUID =
550
+ typeof callKitUUIDOrHeaders === 'string' ? callKitUUIDOrHeaders.toLowerCase() : undefined;
551
+ const actionKey = normalizedUUID ?? LEGACY_CALLKIT_ANSWER_KEY;
552
+ let retainedUUIDOrHeaders = normalizedUUID;
553
+ if (callKitUUIDOrHeaders && typeof callKitUUIDOrHeaders !== 'string') {
554
+ retainedUUIDOrHeaders = { ...callKitUUIDOrHeaders };
555
+ }
556
+ this._pendingCallKitAnswers.set(actionKey, {
557
+ callKitUUIDOrHeaders: retainedUUIDOrHeaders,
558
+ customHeaders: { ...customHeaders },
559
+ });
418
560
  }
419
561
  }
420
562
  /**
421
563
  * Queue an end action for when the call invite arrives (for CallKit integration)
422
564
  * This should be called when the user ends from CallKit before the socket connection is established
423
565
  */
424
- queueEndFromCallKit() {
566
+ queueEndFromCallKit(callKitUUID) {
425
567
  this._throwIfDisposed();
426
568
  if (this._options.debug) {
427
569
  console.log('TelnyxVoipClient: Queuing end action from CallKit');
428
570
  }
429
571
  const telnyxClient = this._sessionManager.telnyxClient;
430
572
  if (telnyxClient && typeof telnyxClient.queueEndFromCallKit === 'function') {
431
- telnyxClient.queueEndFromCallKit();
573
+ telnyxClient.queueEndFromCallKit(callKitUUID);
432
574
  } else {
433
575
  console.warn(
434
576
  'TelnyxVoipClient: TelnyxRTC client not available or method not found for queueEndFromCallKit'
435
577
  );
436
578
  }
437
579
  }
580
+ /**
581
+ * Associate the next push-delivered INVITE with its app-facing CallKit UUID.
582
+ * The underlying signaling call ID remains unchanged.
583
+ * @internal
584
+ */
585
+ setPushNotificationCallKitUUID(callKitUUID) {
586
+ const telnyxClient = this._sessionManager.telnyxClient;
587
+ if (telnyxClient && typeof telnyxClient.setPushNotificationCallKitUUID === 'function') {
588
+ telnyxClient.setPushNotificationCallKitUUID(callKitUUID);
589
+ }
590
+ }
438
591
  // ========== Lifecycle Methods ==========
439
592
  /**
440
593
  * Dispose of the client and clean up all resources.
@@ -443,18 +596,72 @@ class TelnyxVoipClient {
443
596
  * This is particularly important for background clients that should be
444
597
  * disposed after handling push notifications.
445
598
  */
446
- dispose() {
447
- if (this._disposed) {
448
- return;
599
+ async dispose() {
600
+ if (this._disposePromise) {
601
+ return this._disposePromise;
449
602
  }
450
603
  if (this._options.debug) {
451
604
  console.log('TelnyxVoipClient: Disposing client');
452
605
  }
453
606
  this._disposed = true;
454
- this._callStateController.dispose();
455
- this._sessionManager.dispose();
607
+ this._disposePromise = (async () => {
608
+ try {
609
+ await this._sessionManager.dispose();
610
+ } finally {
611
+ this._pendingCallKitAnswers.clear();
612
+ this._callStateController.dispose();
613
+ }
614
+ })();
615
+ return this._disposePromise;
456
616
  }
457
617
  // ========== Private Methods ==========
618
+ /**
619
+ * Forward answers captured during cold start as soon as SessionManager has
620
+ * created the TelnyxRTC instance. SessionManager invokes its ready callback
621
+ * before connect(), so the UUID-keyed action is present when the INVITE
622
+ * arrives.
623
+ */
624
+ _flushPendingCallKitAnswers() {
625
+ const telnyxClient = this._sessionManager.telnyxClient;
626
+ if (!telnyxClient || typeof telnyxClient.queueAnswerFromCallKit !== 'function') {
627
+ return;
628
+ }
629
+ for (const [actionKey, pendingAnswer] of this._pendingCallKitAnswers) {
630
+ try {
631
+ telnyxClient.queueAnswerFromCallKit(
632
+ pendingAnswer.callKitUUIDOrHeaders,
633
+ pendingAnswer.customHeaders
634
+ );
635
+ this._pendingCallKitAnswers.delete(actionKey);
636
+ } catch (error) {
637
+ console.error('TelnyxVoipClient: Failed to restore pending CallKit answer', {
638
+ actionKey,
639
+ error,
640
+ });
641
+ }
642
+ }
643
+ }
644
+ /**
645
+ * Prefer an explicitly supplied token, otherwise hydrate it from PushKit's
646
+ * native storage. PushKit registration starts in AppDelegate before React
647
+ * mounts, so this removes the race between the JS token event and login.
648
+ */
649
+ async _withNativeVoipPushToken(config) {
650
+ if (react_native_1.Platform.OS !== 'ios' || config.pushNotificationDeviceToken?.trim()) {
651
+ return config;
652
+ }
653
+ const nativePushToken = (await voice_pn_bridge_1.VoicePnBridge.getVoipToken())?.trim();
654
+ if (!nativePushToken) {
655
+ return config;
656
+ }
657
+ if (this._options.debug) {
658
+ console.log('TelnyxVoipClient: Using PushKit token from native storage');
659
+ }
660
+ return {
661
+ ...config,
662
+ pushNotificationDeviceToken: nativePushToken,
663
+ };
664
+ }
458
665
  /**
459
666
  * Store credential configuration for automatic reconnection
460
667
  */
@@ -467,6 +674,10 @@ class TelnyxVoipClient {
467
674
  USE_TRICKLE_ICE_STORAGE_KEY,
468
675
  String(config.useTrickleIce ?? false)
469
676
  );
677
+ await AsyncStorage.setItem(
678
+ PUSH_WHEN_ACTIVE_STORAGE_KEY,
679
+ String(config.pushWhenActive ?? false)
680
+ );
470
681
  await AsyncStorage.setItem(
471
682
  MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
472
683
  String(config.enableMissedCallNotifications ?? false)
@@ -497,6 +708,10 @@ class TelnyxVoipClient {
497
708
  USE_TRICKLE_ICE_STORAGE_KEY,
498
709
  String(config.useTrickleIce ?? false)
499
710
  );
711
+ await AsyncStorage.setItem(
712
+ PUSH_WHEN_ACTIVE_STORAGE_KEY,
713
+ String(config.pushWhenActive ?? false)
714
+ );
500
715
  await AsyncStorage.setItem(
501
716
  MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
502
717
  String(config.enableMissedCallNotifications ?? false)
@@ -549,10 +764,11 @@ function createTelnyxVoipClient(options) {
549
764
  * Disposes the current singleton so that a subsequent call to
550
765
  * `createTelnyxVoipClient()` will create a fresh instance.
551
766
  */
552
- function destroyTelnyxVoipClient() {
553
- if (_sharedInstance) {
554
- _sharedInstance.dispose();
767
+ async function destroyTelnyxVoipClient() {
768
+ const sharedInstance = _sharedInstance;
769
+ if (sharedInstance) {
555
770
  _sharedInstance = null;
771
+ await sharedInstance.dispose();
556
772
  }
557
773
  }
558
774
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",
@@ -30,7 +30,7 @@
30
30
  "dev": "tsc --watch",
31
31
  "test": "jest",
32
32
  "test:watch": "jest --watch",
33
- "lint": "eslint src --ext .ts,.tsx",
33
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts,.tsx",
34
34
  "clean": "rm -rf lib",
35
35
  "docs": "typedoc",
36
36
  "docs:watch": "typedoc --watch",
@@ -98,11 +98,13 @@
98
98
  "@types/jest": "^29.5.0",
99
99
  "@types/react": "~19.0.14",
100
100
  "@types/react-native": "^0.72.8",
101
- "@typescript-eslint/eslint-plugin": "^6.0.0",
102
- "@typescript-eslint/parser": "^6.0.0",
101
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
102
+ "@typescript-eslint/parser": "^8.0.0",
103
103
  "eslint": "^8.0.0",
104
+ "expo-router": "^5.1.0",
104
105
  "jest": "^29.5.0",
105
106
  "prettier": "^3.0.0",
107
+ "react": "^19.0.0",
106
108
  "ts-jest": "^29.1.0",
107
109
  "typedoc": "^0.28.14",
108
110
  "typedoc-plugin-markdown": "^4.9.0",