@telnyx/react-voice-commons-sdk 1.1.0 → 1.2.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.
- package/CHANGELOG.md +38 -1
- package/TelnyxVoiceCommons.podspec +3 -3
- package/ios/CallKitBridge.m +7 -0
- package/ios/CallKitBridge.swift +239 -91
- package/lib/callkit/callkit-coordinator.d.ts +19 -0
- package/lib/callkit/callkit-coordinator.js +105 -5
- package/lib/index.d.ts +4 -1
- package/lib/index.js +9 -0
- package/lib/internal/calls/call-state-controller.d.ts +1 -0
- package/lib/internal/calls/call-state-controller.js +35 -1
- package/lib/internal/ice-servers-storage.d.ts +9 -0
- package/lib/internal/ice-servers-storage.js +37 -0
- package/lib/internal/sdk-identity.d.ts +4 -0
- package/lib/internal/sdk-identity.js +60 -0
- package/lib/internal/session/session-manager.d.ts +7 -2
- package/lib/internal/session/session-manager.js +35 -4
- package/lib/internal/signaling-environment.d.ts +13 -0
- package/lib/internal/signaling-environment.js +18 -0
- package/lib/models/call.d.ts +23 -2
- package/lib/models/call.js +37 -2
- package/lib/models/config.d.ts +28 -0
- package/lib/telnyx-voip-client.d.ts +4 -2
- package/lib/telnyx-voip-client.js +60 -2
- package/package.json +3 -4
- package/src/callkit/callkit-coordinator.ts +129 -7
- package/src/index.ts +6 -1
- package/src/internal/calls/call-state-controller.ts +41 -2
- package/src/internal/ice-servers-storage.ts +37 -0
- package/src/internal/sdk-identity.ts +6 -0
- package/src/internal/session/session-manager.ts +38 -4
- package/src/internal/signaling-environment.ts +20 -0
- package/src/models/call.ts +44 -2
- package/src/models/config.ts +30 -0
- package/src/telnyx-voip-client.ts +60 -3
- package/src/types/telnyx-sdk.d.ts +67 -2
|
@@ -64,6 +64,13 @@ 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
|
+
}
|
|
67
74
|
static getInstance() {
|
|
68
75
|
if (!CallKitCoordinator.instance) {
|
|
69
76
|
CallKitCoordinator.instance = new CallKitCoordinator();
|
|
@@ -76,6 +83,7 @@ class CallKitCoordinator {
|
|
|
76
83
|
// Tracks action+UUID pairs so an end action is not dropped merely because
|
|
77
84
|
// an answer or held action for the same call is still completing.
|
|
78
85
|
this.processingCalls = new Set();
|
|
86
|
+
this.activeOrAnsweringCallUUID = null;
|
|
79
87
|
// Tracks calls that have already been ended in CallKit to prevent duplicate reports
|
|
80
88
|
this.endedCalls = new Set();
|
|
81
89
|
// Tracks calls that have already been reported as connected to prevent duplicate reports
|
|
@@ -187,6 +195,16 @@ class CallKitCoordinator {
|
|
|
187
195
|
displayName || destinationNumber
|
|
188
196
|
);
|
|
189
197
|
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
|
+
}
|
|
190
208
|
this.callMap.set(callKitUUID, call);
|
|
191
209
|
this.setupWebRTCCallListeners(call, callKitUUID);
|
|
192
210
|
call._callKitUUID = callKitUUID;
|
|
@@ -208,6 +226,10 @@ class CallKitCoordinator {
|
|
|
208
226
|
console.warn('CallKitCoordinator: Cannot answer call - no CallKit UUID found');
|
|
209
227
|
return false;
|
|
210
228
|
}
|
|
229
|
+
if (!this.claimActiveCall(callKitUUID)) {
|
|
230
|
+
console.warn('CallKitCoordinator: Another call is already active or answering');
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
211
233
|
console.log(
|
|
212
234
|
'CallKitCoordinator: Answering call from UI using CallKit answer simulation',
|
|
213
235
|
callKitUUID
|
|
@@ -236,6 +258,8 @@ class CallKitCoordinator {
|
|
|
236
258
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
237
259
|
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
238
260
|
console.log('CallKitCoordinator: CallKit answer success');
|
|
261
|
+
} else {
|
|
262
|
+
this.releaseActiveCall(callKitUUID);
|
|
239
263
|
}
|
|
240
264
|
return success;
|
|
241
265
|
} catch (error) {
|
|
@@ -391,6 +415,21 @@ class CallKitCoordinator {
|
|
|
391
415
|
console.log('CallKitCoordinator: Answer action already being processed, skipping duplicate');
|
|
392
416
|
return;
|
|
393
417
|
}
|
|
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
|
+
}
|
|
394
433
|
const call = this.callMap.get(callKitUUID);
|
|
395
434
|
if (!call) {
|
|
396
435
|
console.warn('CallKitCoordinator: No WebRTC call found for CallKit answer action', {
|
|
@@ -399,7 +438,10 @@ class CallKitCoordinator {
|
|
|
399
438
|
availableWebRTCCallIds: Array.from(this.callMap.values()).map((c) => c.callId),
|
|
400
439
|
});
|
|
401
440
|
console.log('CallKitCoordinator: No WebRTC call found, handling as push notification');
|
|
402
|
-
await this.handlePushNotificationAnswer(callKitUUID, event);
|
|
441
|
+
const queued = await this.handlePushNotificationAnswer(callKitUUID, event);
|
|
442
|
+
if (!queued) {
|
|
443
|
+
this.releaseActiveCall(callKitUUID);
|
|
444
|
+
}
|
|
403
445
|
return;
|
|
404
446
|
}
|
|
405
447
|
console.log('CallKitCoordinator: Processing CallKit answer action', {
|
|
@@ -675,6 +717,31 @@ class CallKitCoordinator {
|
|
|
675
717
|
return false;
|
|
676
718
|
}
|
|
677
719
|
}
|
|
720
|
+
/** End a still-pending PushKit CallKit call after a remote hangup. */
|
|
721
|
+
async handlePendingPushCallRemoteEnded(callKitUUID, reason) {
|
|
722
|
+
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
723
|
+
// Do not let an unrelated/stale socket BYE terminate another CallKit call.
|
|
724
|
+
// The UUID is added here only after native CallKit registration succeeds.
|
|
725
|
+
if (!this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
726
|
+
console.log('CallKitCoordinator: Ignoring remote-ended event for unknown push UUID', {
|
|
727
|
+
callKitUUID,
|
|
728
|
+
reason,
|
|
729
|
+
});
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
console.log('CallKitCoordinator: Ending pending push call after remote hangup', {
|
|
733
|
+
callKitUUID,
|
|
734
|
+
reason,
|
|
735
|
+
});
|
|
736
|
+
if (!this.endedCalls.has(callKitUUID)) {
|
|
737
|
+
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.RemoteEnded);
|
|
738
|
+
this.endedCalls.add(callKitUUID);
|
|
739
|
+
}
|
|
740
|
+
this.cleanupCall(callKitUUID);
|
|
741
|
+
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
742
|
+
await this.checkBackgroundDisconnection();
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
678
745
|
/**
|
|
679
746
|
* Handle push notification answer - when user answers from CallKit but we don't have a WebRTC call yet
|
|
680
747
|
* This is the iOS equivalent of the Android FCM handler
|
|
@@ -698,7 +765,7 @@ class CallKitCoordinator {
|
|
|
698
765
|
// checkForInitialPushNotification() will run after setVoipClient()
|
|
699
766
|
// and will find the push data still intact, call handleCallKitPushReceived()
|
|
700
767
|
// which queues the matching auto-answer.
|
|
701
|
-
return;
|
|
768
|
+
return false;
|
|
702
769
|
}
|
|
703
770
|
// Queue one UUID-targeted answer. If the push is already being
|
|
704
771
|
// processed, its eventual INVITE will consume this action. Otherwise
|
|
@@ -706,19 +773,20 @@ class CallKitCoordinator {
|
|
|
706
773
|
voipClient.queueAnswerFromCallKit(callKitUUID);
|
|
707
774
|
if (this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
708
775
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
709
|
-
return;
|
|
776
|
+
return true;
|
|
710
777
|
}
|
|
711
|
-
await this.handleCallKitPushReceived(callKitUUID, event);
|
|
712
|
-
return;
|
|
778
|
+
return await this.handleCallKitPushReceived(callKitUUID, event);
|
|
713
779
|
}
|
|
714
780
|
// For other platforms (shouldn't happen on iOS)
|
|
715
781
|
console.error('CallKitCoordinator: ❌ Unsupported platform for push notification handling');
|
|
716
782
|
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
783
|
+
return false;
|
|
717
784
|
} catch (error) {
|
|
718
785
|
console.error('CallKitCoordinator: ❌ Error handling push notification answer:', error);
|
|
719
786
|
// Report the call as failed to CallKit
|
|
720
787
|
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
721
788
|
this.cleanupCall(callKitUUID);
|
|
789
|
+
return false;
|
|
722
790
|
}
|
|
723
791
|
}
|
|
724
792
|
/**
|
|
@@ -764,6 +832,18 @@ class CallKitCoordinator {
|
|
|
764
832
|
});
|
|
765
833
|
switch (state) {
|
|
766
834
|
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
|
+
}
|
|
767
847
|
this.selectCall(call);
|
|
768
848
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
769
849
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
@@ -838,6 +918,9 @@ class CallKitCoordinator {
|
|
|
838
918
|
this.connectedCalls.delete(callKitUUID);
|
|
839
919
|
this.pendingPushCallUUIDs.delete(callKitUUID);
|
|
840
920
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
921
|
+
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
922
|
+
this.activeOrAnsweringCallUUID = null;
|
|
923
|
+
}
|
|
841
924
|
// Get the call before removing it
|
|
842
925
|
const call = this.callMap.get(callKitUUID);
|
|
843
926
|
// Clean up state listeners
|
|
@@ -967,6 +1050,12 @@ class CallKitCoordinator {
|
|
|
967
1050
|
actionKey(action, callKitUUID) {
|
|
968
1051
|
return `${action}:${this.normalizeUUID(callKitUUID)}`;
|
|
969
1052
|
}
|
|
1053
|
+
releaseActiveCall(callKitUUID) {
|
|
1054
|
+
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
1055
|
+
if (this.activeOrAnsweringCallUUID === callKitUUID) {
|
|
1056
|
+
this.activeOrAnsweringCallUUID = null;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
970
1059
|
selectCall(call) {
|
|
971
1060
|
const callKitUUID = this.getCallKitUUID(call);
|
|
972
1061
|
const wrapperCall = this.voipClient?.findCallByTelnyxCall(call);
|
|
@@ -1174,6 +1263,9 @@ class CallKitCoordinator {
|
|
|
1174
1263
|
resetFlags() {
|
|
1175
1264
|
console.log('CallKitCoordinator: Resetting coordinator flags');
|
|
1176
1265
|
this.isCallFromPush = this.pendingPushCallUUIDs.size > 0;
|
|
1266
|
+
if (this.callMap.size === 0) {
|
|
1267
|
+
this.activeOrAnsweringCallUUID = null;
|
|
1268
|
+
}
|
|
1177
1269
|
console.log('CallKitCoordinator: ✅ Coordinator flags reset');
|
|
1178
1270
|
}
|
|
1179
1271
|
/**
|
|
@@ -1187,6 +1279,14 @@ class CallKitCoordinator {
|
|
|
1187
1279
|
// call arrives during push notification handling.
|
|
1188
1280
|
return this.processingCalls.size > 0 || this.isCallFromPush;
|
|
1189
1281
|
}
|
|
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
|
+
}
|
|
1190
1290
|
/**
|
|
1191
1291
|
* Check if there's currently a call from push notification being processed
|
|
1192
1292
|
* This helps prevent disconnection during push call handling
|
package/lib/index.d.ts
CHANGED
|
@@ -38,8 +38,11 @@ export {
|
|
|
38
38
|
createCredentialConfig,
|
|
39
39
|
createTokenConfig,
|
|
40
40
|
} from './models/config';
|
|
41
|
-
export type { Config, CredentialConfig, TokenConfig } from './models/config';
|
|
41
|
+
export type { Config, CredentialConfig, TokenConfig, TelnyxEnvironment } from './models/config';
|
|
42
|
+
export type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
42
43
|
export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
44
|
+
export { CallQualityLevel } from '@telnyx/react-native-voice-sdk';
|
|
45
|
+
export type { CallQualityMetrics, QualityStatsRecord } from '@telnyx/react-native-voice-sdk';
|
|
43
46
|
export * from './callkit';
|
|
44
47
|
export { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
45
48
|
export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var __exportStar =
|
|
|
39
39
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
40
40
|
exports.useAppReadyNotifier =
|
|
41
41
|
exports.VoicePnBridge =
|
|
42
|
+
exports.CallQualityLevel =
|
|
42
43
|
exports.createTokenConfig =
|
|
43
44
|
exports.createCredentialConfig =
|
|
44
45
|
exports.validateTokenConfig =
|
|
@@ -214,6 +215,14 @@ Object.defineProperty(exports, 'createTokenConfig', {
|
|
|
214
215
|
return config_1.createTokenConfig;
|
|
215
216
|
},
|
|
216
217
|
});
|
|
218
|
+
// Re-export call quality metrics types from the underlying SDK
|
|
219
|
+
var react_native_voice_sdk_1 = require('@telnyx/react-native-voice-sdk');
|
|
220
|
+
Object.defineProperty(exports, 'CallQualityLevel', {
|
|
221
|
+
enumerable: true,
|
|
222
|
+
get: function () {
|
|
223
|
+
return react_native_voice_sdk_1.CallQualityLevel;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
217
226
|
// Export CallKit functionality
|
|
218
227
|
__exportStar(require('./callkit'), exports);
|
|
219
228
|
// Native push notification helpers
|
|
@@ -122,6 +122,7 @@ export declare class CallStateController {
|
|
|
122
122
|
private _removeClientListeners;
|
|
123
123
|
private _handleTelnyxIncomingCall;
|
|
124
124
|
private _handleTelnyxReattachedCall;
|
|
125
|
+
private _handleRemoteCallEnded;
|
|
125
126
|
private _handleTelnyxCallStateChanged;
|
|
126
127
|
private _handleTelnyxCallRemoved;
|
|
127
128
|
/**
|
|
@@ -31,6 +31,19 @@ class CallStateController {
|
|
|
31
31
|
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
32
32
|
this._handleIncomingCall(telnyxCall, msg, true);
|
|
33
33
|
};
|
|
34
|
+
this._handleRemoteCallEnded = (callId, reason) => {
|
|
35
|
+
console.log('CallStateController: Pending push call ended remotely', {
|
|
36
|
+
callId,
|
|
37
|
+
reason,
|
|
38
|
+
});
|
|
39
|
+
// A tracked call can also reach the existing state-listener teardown path.
|
|
40
|
+
// Both paths are intentionally idempotent so pre-INVITE calls are covered too.
|
|
41
|
+
void callkit_coordinator_1.callKitCoordinator
|
|
42
|
+
.handlePendingPushCallRemoteEnded(callId, reason)
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
console.error('CallStateController: Failed to end pending push CallKit call:', error);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
34
47
|
this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
|
|
35
48
|
console.log(
|
|
36
49
|
'CallStateController: Call state changed from TelnyxRTC:',
|
|
@@ -184,8 +197,24 @@ class CallStateController {
|
|
|
184
197
|
* This should be called by the session manager after client creation
|
|
185
198
|
*/
|
|
186
199
|
initializeClientListeners() {
|
|
200
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
187
201
|
console.log('CallStateController: initializeClientListeners called');
|
|
188
|
-
console.log('CallStateController: Current client exists:', !!
|
|
202
|
+
console.log('CallStateController: Current client exists:', !!telnyxClient);
|
|
203
|
+
if (!telnyxClient) {
|
|
204
|
+
console.log('CallStateController: No telnyxClient available, skipping listener setup');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Skip redundant listener setup — SessionManager calls _onClientReady
|
|
208
|
+
// twice during connect/reconnect (once before connection, once on
|
|
209
|
+
// telnyx.client.ready event). _setupClientListeners() already
|
|
210
|
+
// removes old listeners before re-adding, so this guard skips the
|
|
211
|
+
// redundant off→on churn rather than preventing actual duplicates.
|
|
212
|
+
if (this._listenerClient === telnyxClient) {
|
|
213
|
+
console.log(
|
|
214
|
+
'CallStateController: Listeners already registered for this client instance, skipping'
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
189
218
|
this._setupClientListeners();
|
|
190
219
|
// CallKit integration now handled by CallKitCoordinator
|
|
191
220
|
console.log('CallStateController: Using CallKitCoordinator for CallKit integration');
|
|
@@ -320,6 +349,10 @@ class CallStateController {
|
|
|
320
349
|
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
321
350
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
322
351
|
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
352
|
+
// A caller can hang up while PushKit is reconnecting the socket, before
|
|
353
|
+
// an INVITE creates a Call object. Forward that terminal event so the
|
|
354
|
+
// already-presented CallKit call is dismissed.
|
|
355
|
+
telnyxClient.on('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
323
356
|
console.log('CallStateController: Client listeners set up successfully');
|
|
324
357
|
}
|
|
325
358
|
_removeClientListeners() {
|
|
@@ -330,6 +363,7 @@ class CallStateController {
|
|
|
330
363
|
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
331
364
|
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
332
365
|
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
366
|
+
this._listenerClient.off('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
333
367
|
this._listenerClient = undefined;
|
|
334
368
|
}
|
|
335
369
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
2
|
+
export declare const ICE_SERVERS_STORAGE_KEY = '@ice_servers';
|
|
3
|
+
/**
|
|
4
|
+
* Parse the ICE servers persisted by a previous login. A missing, malformed,
|
|
5
|
+
* or structurally invalid value yields `undefined` (the core SDK then uses the
|
|
6
|
+
* Telnyx defaults) rather than throwing, so a bad stored list never discards
|
|
7
|
+
* otherwise valid stored authentication.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseStoredIceServers(stored: string | null): TelnyxIceServer[] | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.ICE_SERVERS_STORAGE_KEY = void 0;
|
|
4
|
+
exports.parseStoredIceServers = parseStoredIceServers;
|
|
5
|
+
exports.ICE_SERVERS_STORAGE_KEY = '@ice_servers';
|
|
6
|
+
/**
|
|
7
|
+
* Parse the ICE servers persisted by a previous login. A missing, malformed,
|
|
8
|
+
* or structurally invalid value yields `undefined` (the core SDK then uses the
|
|
9
|
+
* Telnyx defaults) rather than throwing, so a bad stored list never discards
|
|
10
|
+
* otherwise valid stored authentication.
|
|
11
|
+
*/
|
|
12
|
+
function parseStoredIceServers(stored) {
|
|
13
|
+
if (!stored) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(stored);
|
|
18
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isIceServer)) {
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Fall through to the warning below.
|
|
23
|
+
}
|
|
24
|
+
console.warn('Ignoring invalid stored ICE servers; using the SDK defaults');
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function isIceServer(value) {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const server = value;
|
|
32
|
+
return (
|
|
33
|
+
typeof server.urls === 'string' &&
|
|
34
|
+
(server.username === undefined || typeof server.username === 'string') &&
|
|
35
|
+
(server.credential === undefined || typeof server.credential === 'string')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
55
|
+
exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = void 0;
|
|
56
|
+
const pkg = __importStar(require('../../package.json'));
|
|
57
|
+
exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = Object.freeze({
|
|
58
|
+
name: 'webrtc-react-native-commons',
|
|
59
|
+
version: pkg.version,
|
|
60
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import * as TelnyxSDK from '@telnyx/react-native-voice-sdk';
|
|
3
3
|
import { TelnyxConnectionState } from '../../models/connection-state';
|
|
4
|
-
import { Config, CredentialConfig, TokenConfig } from '../../models/config';
|
|
4
|
+
import { Config, CredentialConfig, TelnyxEnvironment, TokenConfig } from '../../models/config';
|
|
5
|
+
export interface SessionManagerOptions {
|
|
6
|
+
/** Environment used when a config or stored restore does not specify one */
|
|
7
|
+
defaultEnvironment?: TelnyxEnvironment;
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
7
11
|
*
|
|
@@ -20,7 +24,8 @@ export declare class SessionManager {
|
|
|
20
24
|
private _connectPromise?;
|
|
21
25
|
private _onClientReady?;
|
|
22
26
|
private _onDisconnect?;
|
|
23
|
-
|
|
27
|
+
private readonly _defaultEnvironment;
|
|
28
|
+
constructor(options?: SessionManagerOptions);
|
|
24
29
|
/**
|
|
25
30
|
* Observable stream of connection state changes
|
|
26
31
|
*/
|
|
@@ -56,12 +56,15 @@ exports.SessionManager = void 0;
|
|
|
56
56
|
const rxjs_1 = require('rxjs');
|
|
57
57
|
const operators_1 = require('rxjs/operators');
|
|
58
58
|
const TelnyxSDK = __importStar(require('@telnyx/react-native-voice-sdk'));
|
|
59
|
-
const
|
|
59
|
+
const sdk_identity_1 = require('../sdk-identity');
|
|
60
|
+
const ice_servers_storage_1 = require('../ice-servers-storage');
|
|
61
|
+
const signaling_environment_1 = require('../signaling-environment');
|
|
60
62
|
const connection_state_1 = require('../../models/connection-state');
|
|
61
63
|
const config_1 = require('../../models/config');
|
|
62
64
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
63
65
|
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
64
66
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
67
|
+
const QUALITY_METRICS_STORAGE_KEY = '@enable_quality_metrics';
|
|
65
68
|
/**
|
|
66
69
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
67
70
|
*
|
|
@@ -69,7 +72,7 @@ const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications
|
|
|
69
72
|
* and automatic reconnection logic.
|
|
70
73
|
*/
|
|
71
74
|
class SessionManager {
|
|
72
|
-
constructor() {
|
|
75
|
+
constructor(options = {}) {
|
|
73
76
|
this._connectionState = new rxjs_1.BehaviorSubject(
|
|
74
77
|
connection_state_1.TelnyxConnectionState.DISCONNECTED
|
|
75
78
|
);
|
|
@@ -77,6 +80,7 @@ class SessionManager {
|
|
|
77
80
|
this._disposing = false;
|
|
78
81
|
this._connectionGeneration = 0;
|
|
79
82
|
this._sessionId = this._generateSessionId();
|
|
83
|
+
this._defaultEnvironment = options.defaultEnvironment ?? 'production';
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Observable stream of connection state changes
|
|
@@ -254,9 +258,22 @@ class SessionManager {
|
|
|
254
258
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
255
259
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
256
260
|
);
|
|
261
|
+
const storedEnvironment = await AsyncStorage.getItem(
|
|
262
|
+
signaling_environment_1.SIGNALING_ENVIRONMENT_STORAGE_KEY
|
|
263
|
+
);
|
|
264
|
+
const storedQualityMetrics = await AsyncStorage.getItem(QUALITY_METRICS_STORAGE_KEY);
|
|
265
|
+
const storedIceServers = await AsyncStorage.getItem(
|
|
266
|
+
ice_servers_storage_1.ICE_SERVERS_STORAGE_KEY
|
|
267
|
+
);
|
|
268
|
+
const iceServers = (0, ice_servers_storage_1.parseStoredIceServers)(storedIceServers);
|
|
257
269
|
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
258
270
|
const pushWhenActive = storedPushWhenActive === 'true';
|
|
259
271
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
272
|
+
const enableQualityMetrics = storedQualityMetrics === 'true';
|
|
273
|
+
const environment = (0, signaling_environment_1.resolveStoredEnvironment)(
|
|
274
|
+
storedEnvironment,
|
|
275
|
+
this._defaultEnvironment
|
|
276
|
+
);
|
|
260
277
|
if (this._isTeardownActive()) {
|
|
261
278
|
return;
|
|
262
279
|
}
|
|
@@ -269,6 +286,9 @@ class SessionManager {
|
|
|
269
286
|
useTrickleIce,
|
|
270
287
|
pushWhenActive,
|
|
271
288
|
enableMissedCallNotifications,
|
|
289
|
+
enableQualityMetrics,
|
|
290
|
+
environment,
|
|
291
|
+
iceServers,
|
|
272
292
|
});
|
|
273
293
|
}
|
|
274
294
|
// Check if we have token-based authentication data
|
|
@@ -280,6 +300,9 @@ class SessionManager {
|
|
|
280
300
|
useTrickleIce,
|
|
281
301
|
pushWhenActive,
|
|
282
302
|
enableMissedCallNotifications,
|
|
303
|
+
enableQualityMetrics,
|
|
304
|
+
environment,
|
|
305
|
+
iceServers,
|
|
283
306
|
});
|
|
284
307
|
}
|
|
285
308
|
if (this._currentConfig) {
|
|
@@ -463,11 +486,15 @@ class SessionManager {
|
|
|
463
486
|
pushWhenActive: config.pushWhenActive,
|
|
464
487
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
465
488
|
useTrickleIce: config.useTrickleIce,
|
|
489
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
466
490
|
enableCallReports: config.enableCallReports,
|
|
491
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
467
492
|
callReportInterval: config.callReportInterval,
|
|
468
493
|
callReportLogLevel: config.callReportLogLevel,
|
|
469
494
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
470
|
-
|
|
495
|
+
iceServers: config.iceServers,
|
|
496
|
+
sdkName: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
497
|
+
sdkVersion: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
471
498
|
};
|
|
472
499
|
console.log(
|
|
473
500
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
@@ -484,11 +511,15 @@ class SessionManager {
|
|
|
484
511
|
pushWhenActive: config.pushWhenActive,
|
|
485
512
|
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
486
513
|
useTrickleIce: config.useTrickleIce,
|
|
514
|
+
environment: config.environment ?? this._defaultEnvironment,
|
|
487
515
|
enableCallReports: config.enableCallReports,
|
|
516
|
+
enableQualityMetrics: config.enableQualityMetrics,
|
|
488
517
|
callReportInterval: config.callReportInterval,
|
|
489
518
|
callReportLogLevel: config.callReportLogLevel,
|
|
490
519
|
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
491
|
-
|
|
520
|
+
iceServers: config.iceServers,
|
|
521
|
+
sdkName: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.name,
|
|
522
|
+
sdkVersion: sdk_identity_1.REACT_NATIVE_COMMONS_SDK_IDENTITY.version,
|
|
492
523
|
};
|
|
493
524
|
console.log(
|
|
494
525
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TelnyxEnvironment } from '../models/config';
|
|
2
|
+
export declare const SIGNALING_ENVIRONMENT_STORAGE_KEY = '@telnyx_signaling_environment';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the signaling environment for a stored-config restore.
|
|
5
|
+
*
|
|
6
|
+
* An explicit stored selection always wins. A missing or invalid stored value
|
|
7
|
+
* (e.g. credentials persisted by an older Commons version) falls back to the
|
|
8
|
+
* client's configured default, and only then to production.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveStoredEnvironment(
|
|
11
|
+
storedEnvironment: string | null | undefined,
|
|
12
|
+
defaultEnvironment?: TelnyxEnvironment
|
|
13
|
+
): TelnyxEnvironment;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.SIGNALING_ENVIRONMENT_STORAGE_KEY = void 0;
|
|
4
|
+
exports.resolveStoredEnvironment = resolveStoredEnvironment;
|
|
5
|
+
exports.SIGNALING_ENVIRONMENT_STORAGE_KEY = '@telnyx_signaling_environment';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the signaling environment for a stored-config restore.
|
|
8
|
+
*
|
|
9
|
+
* An explicit stored selection always wins. A missing or invalid stored value
|
|
10
|
+
* (e.g. credentials persisted by an older Commons version) falls back to the
|
|
11
|
+
* client's configured default, and only then to production.
|
|
12
|
+
*/
|
|
13
|
+
function resolveStoredEnvironment(storedEnvironment, defaultEnvironment) {
|
|
14
|
+
if (storedEnvironment === 'development' || storedEnvironment === 'production') {
|
|
15
|
+
return storedEnvironment;
|
|
16
|
+
}
|
|
17
|
+
return defaultEnvironment ?? 'production';
|
|
18
|
+
}
|