@telnyx/react-voice-commons-sdk 1.0.0 → 1.1.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 +23 -0
- package/README.md +28 -7
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +19 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +19 -0
- package/ios/CallKitBridge.m +27 -1
- package/ios/CallKitBridge.swift +640 -125
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +6 -0
- package/ios/VoicePnBridge.swift +15 -0
- package/lib/callkit/callkit-coordinator.d.ts +39 -3
- package/lib/callkit/callkit-coordinator.js +561 -148
- package/lib/callkit/callkit.d.ts +7 -0
- package/lib/callkit/callkit.js +83 -11
- package/lib/hooks/useAppStateHandler.js +6 -2
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +33 -1
- package/lib/internal/calls/call-state-controller.js +100 -28
- package/lib/internal/session/session-manager.d.ts +15 -1
- package/lib/internal/session/session-manager.js +214 -62
- package/lib/internal/voice-pn-bridge.d.ts +18 -0
- package/lib/internal/voice-pn-bridge.js +39 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +10 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +46 -5
- package/lib/telnyx-voip-client.js +238 -22
- package/package.json +7 -5
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +112 -29
- package/src/internal/session/session-manager.ts +260 -65
- package/src/internal/voice-pn-bridge.ts +47 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +10 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +216 -24
- package/src/types/telnyx-sdk.d.ts +2 -1
|
@@ -60,6 +60,7 @@ const pkg = __importStar(require('../../../package.json'));
|
|
|
60
60
|
const connection_state_1 = require('../../models/connection-state');
|
|
61
61
|
const config_1 = require('../../models/config');
|
|
62
62
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
63
|
+
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
63
64
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
64
65
|
/**
|
|
65
66
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
@@ -73,6 +74,8 @@ class SessionManager {
|
|
|
73
74
|
connection_state_1.TelnyxConnectionState.DISCONNECTED
|
|
74
75
|
);
|
|
75
76
|
this._disposed = false;
|
|
77
|
+
this._disposing = false;
|
|
78
|
+
this._connectionGeneration = 0;
|
|
76
79
|
this._sessionId = this._generateSessionId();
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
@@ -119,21 +122,19 @@ class SessionManager {
|
|
|
119
122
|
* Connect using credential authentication
|
|
120
123
|
*/
|
|
121
124
|
async connectWithCredential(config) {
|
|
122
|
-
|
|
123
|
-
throw new Error('SessionManager has been disposed');
|
|
124
|
-
}
|
|
125
|
+
this._assertCanStartConnection();
|
|
125
126
|
this._currentConfig = config;
|
|
126
127
|
await this._connect();
|
|
128
|
+
this._assertCanStartConnection();
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
131
|
* Connect using token authentication
|
|
130
132
|
*/
|
|
131
133
|
async connectWithToken(config) {
|
|
132
|
-
|
|
133
|
-
throw new Error('SessionManager has been disposed');
|
|
134
|
-
}
|
|
134
|
+
this._assertCanStartConnection();
|
|
135
135
|
this._currentConfig = config;
|
|
136
136
|
await this._connect();
|
|
137
|
+
this._assertCanStartConnection();
|
|
137
138
|
}
|
|
138
139
|
/**
|
|
139
140
|
* Disconnect from the Telnyx platform.
|
|
@@ -149,6 +150,7 @@ class SessionManager {
|
|
|
149
150
|
if (this._disposed) {
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
153
|
+
this._connectionGeneration += 1;
|
|
152
154
|
this._currentConfig = undefined;
|
|
153
155
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
154
156
|
if (this._onDisconnect) {
|
|
@@ -159,11 +161,7 @@ class SessionManager {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
if (this._telnyxClient) {
|
|
162
|
-
|
|
163
|
-
await this._telnyxClient.disconnect();
|
|
164
|
-
} catch (error) {
|
|
165
|
-
console.error('Error during disconnect:', error);
|
|
166
|
-
}
|
|
164
|
+
await this._disconnectAndForgetClient(this._telnyxClient, 'Error during disconnect:');
|
|
167
165
|
}
|
|
168
166
|
}
|
|
169
167
|
/**
|
|
@@ -173,6 +171,7 @@ class SessionManager {
|
|
|
173
171
|
*/
|
|
174
172
|
disablePushNotifications() {
|
|
175
173
|
if (
|
|
174
|
+
!this._isTeardownActive() &&
|
|
176
175
|
this._telnyxClient &&
|
|
177
176
|
this.currentState === connection_state_1.TelnyxConnectionState.CONNECTED
|
|
178
177
|
) {
|
|
@@ -186,7 +185,7 @@ class SessionManager {
|
|
|
186
185
|
* Handle push notification with stored config
|
|
187
186
|
*/
|
|
188
187
|
handlePushNotificationWithConfig(pushMetaData, config) {
|
|
189
|
-
if (this.
|
|
188
|
+
if (this._isTeardownActive()) {
|
|
190
189
|
return;
|
|
191
190
|
}
|
|
192
191
|
this._currentConfig = config;
|
|
@@ -198,7 +197,7 @@ class SessionManager {
|
|
|
198
197
|
* Handle push notification (async version)
|
|
199
198
|
*/
|
|
200
199
|
async handlePushNotification(payload) {
|
|
201
|
-
if (this.
|
|
200
|
+
if (this._isTeardownActive()) {
|
|
202
201
|
return;
|
|
203
202
|
}
|
|
204
203
|
console.log(
|
|
@@ -207,19 +206,33 @@ class SessionManager {
|
|
|
207
206
|
);
|
|
208
207
|
// Store the push notification payload for when the client is created
|
|
209
208
|
this._pendingPushPayload = payload;
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} catch (err) {
|
|
219
|
-
console.warn('SessionManager: disconnect of prior client threw:', err);
|
|
209
|
+
// A second-call push must stay on the client that owns the active/held
|
|
210
|
+
// call. Tearing it down destroys the first signaling session and clears
|
|
211
|
+
// the call collection before CallKit can complete Hold & Accept.
|
|
212
|
+
if (this._telnyxClient && this._hasActiveOrHeldCall(this._telnyxClient)) {
|
|
213
|
+
const actualPayload = this._extractPushPayload(payload);
|
|
214
|
+
const processVoIPNotification = this._telnyxClient.processVoIPNotification;
|
|
215
|
+
if (typeof processVoIPNotification !== 'function') {
|
|
216
|
+
throw new Error('TelnyxRTC client cannot process an active-call VoIP notification');
|
|
220
217
|
}
|
|
218
|
+
console.log(
|
|
219
|
+
'SessionManager: Preserving active client while processing second-call push notification'
|
|
220
|
+
);
|
|
221
|
+
processVoIPNotification.call(this._telnyxClient, actualPayload);
|
|
222
|
+
this._pendingPushPayload = null;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// The cold/no-active-call path still rebuilds the client so the socket is
|
|
226
|
+
// stamped with the push voice_sdk_id before connecting.
|
|
227
|
+
if (this._telnyxClient) {
|
|
228
|
+
await this._disconnectAndForgetClient(
|
|
229
|
+
this._telnyxClient,
|
|
230
|
+
'SessionManager: disconnect of prior client threw:'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (this._isTeardownActive()) {
|
|
234
|
+
return;
|
|
221
235
|
}
|
|
222
|
-
this._telnyxClient = undefined;
|
|
223
236
|
if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
|
|
224
237
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
225
238
|
}
|
|
@@ -237,11 +250,16 @@ class SessionManager {
|
|
|
237
250
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
238
251
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
239
252
|
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
253
|
+
const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
|
|
240
254
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
241
255
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
242
256
|
);
|
|
243
257
|
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
258
|
+
const pushWhenActive = storedPushWhenActive === 'true';
|
|
244
259
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
260
|
+
if (this._isTeardownActive()) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
245
263
|
// Check if we have credential-based authentication data
|
|
246
264
|
if (storedUsername && storedPassword) {
|
|
247
265
|
console.log('SessionManager: RELEASE DEBUG - Found stored credentials, creating config');
|
|
@@ -249,6 +267,7 @@ class SessionManager {
|
|
|
249
267
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
250
268
|
pushNotificationDeviceToken: storedPushToken,
|
|
251
269
|
useTrickleIce,
|
|
270
|
+
pushWhenActive,
|
|
252
271
|
enableMissedCallNotifications,
|
|
253
272
|
});
|
|
254
273
|
}
|
|
@@ -259,6 +278,7 @@ class SessionManager {
|
|
|
259
278
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
260
279
|
pushNotificationDeviceToken: storedPushToken,
|
|
261
280
|
useTrickleIce,
|
|
281
|
+
pushWhenActive,
|
|
262
282
|
enableMissedCallNotifications,
|
|
263
283
|
});
|
|
264
284
|
}
|
|
@@ -273,6 +293,9 @@ class SessionManager {
|
|
|
273
293
|
console.warn('SessionManager: Failed to load stored config for push notification:', error);
|
|
274
294
|
}
|
|
275
295
|
}
|
|
296
|
+
if (this._isTeardownActive()) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
276
299
|
// If we already have a client, process the push notification immediately
|
|
277
300
|
if (this._telnyxClient) {
|
|
278
301
|
console.log(
|
|
@@ -317,10 +340,16 @@ class SessionManager {
|
|
|
317
340
|
);
|
|
318
341
|
try {
|
|
319
342
|
await this._connect();
|
|
343
|
+
if (this._isTeardownActive()) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
320
346
|
console.log(
|
|
321
347
|
'SessionManager: RELEASE DEBUG - Successfully connected after push notification trigger'
|
|
322
348
|
);
|
|
323
349
|
} catch (error) {
|
|
350
|
+
if (this._isTeardownActive()) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
324
353
|
console.error(
|
|
325
354
|
'SessionManager: Failed to connect after push notification trigger:',
|
|
326
355
|
error
|
|
@@ -340,77 +369,138 @@ class SessionManager {
|
|
|
340
369
|
}
|
|
341
370
|
console.log('SessionManager: RELEASE DEBUG - Push notification handling complete');
|
|
342
371
|
}
|
|
372
|
+
_hasActiveOrHeldCall(client) {
|
|
373
|
+
const calls =
|
|
374
|
+
typeof client.getActiveCalls === 'function'
|
|
375
|
+
? client.getActiveCalls()
|
|
376
|
+
: Array.from(client.calls?.values?.() || []);
|
|
377
|
+
return calls.some((call) => call?.state === 'active' || call?.state === 'held');
|
|
378
|
+
}
|
|
343
379
|
/**
|
|
344
380
|
* Dispose of the session manager and clean up resources
|
|
345
381
|
*/
|
|
346
|
-
dispose() {
|
|
382
|
+
async dispose() {
|
|
347
383
|
if (this._disposed) {
|
|
348
384
|
return;
|
|
349
385
|
}
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
386
|
+
if (!this._disposePromise) {
|
|
387
|
+
this._disposePromise = this._dispose();
|
|
388
|
+
}
|
|
389
|
+
await this._disposePromise;
|
|
353
390
|
}
|
|
354
391
|
/**
|
|
355
392
|
* Internal method to establish connection with or without push notification handling
|
|
356
393
|
*/
|
|
357
394
|
async _connect() {
|
|
358
|
-
|
|
395
|
+
const previousConnect = this._connectPromise;
|
|
396
|
+
if (previousConnect) {
|
|
397
|
+
try {
|
|
398
|
+
await previousConnect;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (this._isTeardownActive()) {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
this._assertCanStartConnection();
|
|
406
|
+
const config = this._currentConfig;
|
|
407
|
+
if (!config) {
|
|
359
408
|
throw new Error('No configuration provided');
|
|
360
409
|
}
|
|
410
|
+
const connectionGeneration = (this._connectionGeneration += 1);
|
|
411
|
+
const connectPromise = this._runConnect(config, connectionGeneration);
|
|
412
|
+
this._connectPromise = connectPromise;
|
|
413
|
+
try {
|
|
414
|
+
await connectPromise;
|
|
415
|
+
} finally {
|
|
416
|
+
if (this._connectPromise === connectPromise) {
|
|
417
|
+
this._connectPromise = undefined;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async _dispose() {
|
|
422
|
+
this._disposing = true;
|
|
423
|
+
this._connectionGeneration += 1;
|
|
424
|
+
const inFlightConnect = this._connectPromise;
|
|
425
|
+
await this.disconnect();
|
|
426
|
+
if (inFlightConnect) {
|
|
427
|
+
try {
|
|
428
|
+
await inFlightConnect;
|
|
429
|
+
} catch {
|
|
430
|
+
// A connect canceled by disposal is expected; the cleanup path runs in _runConnect.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const client = this._telnyxClient;
|
|
434
|
+
if (client) {
|
|
435
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
436
|
+
}
|
|
437
|
+
this._currentConfig = undefined;
|
|
438
|
+
this._pendingPushPayload = null;
|
|
439
|
+
this._disposed = true;
|
|
440
|
+
this._disposing = false;
|
|
441
|
+
this._connectionState.complete();
|
|
442
|
+
}
|
|
443
|
+
async _runConnect(config, connectionGeneration) {
|
|
444
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
361
445
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTING);
|
|
446
|
+
let client;
|
|
447
|
+
let canceledConnectCleanupCompleted = false;
|
|
362
448
|
try {
|
|
363
449
|
// Clean up existing client
|
|
364
450
|
if (this._telnyxClient) {
|
|
365
|
-
await this._telnyxClient
|
|
451
|
+
await this._disconnectClient(this._telnyxClient, 'Error during disconnect:');
|
|
366
452
|
}
|
|
453
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
367
454
|
// Create new client instance with authentication options
|
|
368
455
|
let clientOptions;
|
|
369
|
-
if ((0, config_1.isCredentialConfig)(
|
|
456
|
+
if ((0, config_1.isCredentialConfig)(config)) {
|
|
370
457
|
clientOptions = {
|
|
371
|
-
login:
|
|
372
|
-
password:
|
|
373
|
-
logLevel:
|
|
374
|
-
debug:
|
|
375
|
-
pushNotificationDeviceToken:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
458
|
+
login: config.sipUser,
|
|
459
|
+
password: config.sipPassword,
|
|
460
|
+
logLevel: config.debug ? 'debug' : 'warn',
|
|
461
|
+
debug: config.debug ?? false,
|
|
462
|
+
pushNotificationDeviceToken: config.pushNotificationDeviceToken,
|
|
463
|
+
pushWhenActive: config.pushWhenActive,
|
|
464
|
+
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
465
|
+
useTrickleIce: config.useTrickleIce,
|
|
466
|
+
enableCallReports: config.enableCallReports,
|
|
467
|
+
callReportInterval: config.callReportInterval,
|
|
468
|
+
callReportLogLevel: config.callReportLogLevel,
|
|
469
|
+
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
382
470
|
sdkVersion: pkg.version,
|
|
383
471
|
};
|
|
384
472
|
console.log(
|
|
385
473
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
386
474
|
clientOptions.logLevel,
|
|
387
475
|
'pushToken:',
|
|
388
|
-
!!
|
|
476
|
+
!!config.pushNotificationDeviceToken
|
|
389
477
|
);
|
|
390
|
-
} else if ((0, config_1.isTokenConfig)(
|
|
478
|
+
} else if ((0, config_1.isTokenConfig)(config)) {
|
|
391
479
|
clientOptions = {
|
|
392
|
-
login_token:
|
|
393
|
-
logLevel:
|
|
394
|
-
debug:
|
|
395
|
-
pushNotificationDeviceToken:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
480
|
+
login_token: config.token,
|
|
481
|
+
logLevel: config.debug ? 'debug' : 'warn',
|
|
482
|
+
debug: config.debug ?? false,
|
|
483
|
+
pushNotificationDeviceToken: config.pushNotificationDeviceToken,
|
|
484
|
+
pushWhenActive: config.pushWhenActive,
|
|
485
|
+
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
486
|
+
useTrickleIce: config.useTrickleIce,
|
|
487
|
+
enableCallReports: config.enableCallReports,
|
|
488
|
+
callReportInterval: config.callReportInterval,
|
|
489
|
+
callReportLogLevel: config.callReportLogLevel,
|
|
490
|
+
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
402
491
|
sdkVersion: pkg.version,
|
|
403
492
|
};
|
|
404
493
|
console.log(
|
|
405
494
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
406
495
|
clientOptions.logLevel,
|
|
407
496
|
'pushToken:',
|
|
408
|
-
!!
|
|
497
|
+
!!config.pushNotificationDeviceToken
|
|
409
498
|
);
|
|
410
499
|
} else {
|
|
411
500
|
throw new Error('Invalid configuration type');
|
|
412
501
|
}
|
|
413
|
-
|
|
502
|
+
client = new TelnyxSDK.TelnyxRTC(clientOptions);
|
|
503
|
+
this._telnyxClient = client;
|
|
414
504
|
// CRITICAL: Process any pending push notification payload BEFORE connecting
|
|
415
505
|
// This ensures voice_sdk_id and other payload variables are set before connect() is called
|
|
416
506
|
const pendingPushPayload = this._pendingPushPayload;
|
|
@@ -437,7 +527,7 @@ class SessionManager {
|
|
|
437
527
|
// Clear the pending payload
|
|
438
528
|
this._pendingPushPayload = null;
|
|
439
529
|
}
|
|
440
|
-
this._setupClientListeners();
|
|
530
|
+
this._setupClientListeners(client);
|
|
441
531
|
// Set up CallStateController listeners immediately after client creation
|
|
442
532
|
// This ensures they're ready before any incoming call events are emitted
|
|
443
533
|
console.log(
|
|
@@ -451,15 +541,34 @@ class SessionManager {
|
|
|
451
541
|
} else {
|
|
452
542
|
console.log('🔧 SessionManager: No _onClientReady callback found');
|
|
453
543
|
}
|
|
544
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
454
545
|
// Connect to the platform AFTER processing push notification
|
|
455
546
|
console.log(
|
|
456
547
|
'SessionManager: RELEASE DEBUG - About to call connect() after processing push notification'
|
|
457
548
|
);
|
|
458
|
-
await
|
|
549
|
+
await client.connect();
|
|
550
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
551
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
552
|
+
canceledConnectCleanupCompleted = true;
|
|
553
|
+
throw this._createConnectCanceledError();
|
|
554
|
+
}
|
|
459
555
|
// Notify that client is ready for event listeners
|
|
460
556
|
console.log('🔧 SessionManager: Client connected successfully');
|
|
461
557
|
} catch (error) {
|
|
558
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
559
|
+
if (client && !canceledConnectCleanupCompleted) {
|
|
560
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
561
|
+
}
|
|
562
|
+
throw this._createConnectCanceledError();
|
|
563
|
+
}
|
|
462
564
|
console.error('Connection failed:', error);
|
|
565
|
+
if (client) {
|
|
566
|
+
try {
|
|
567
|
+
await this._disconnectAndForgetClient(client, 'Error during failed connect cleanup:');
|
|
568
|
+
} catch (cleanupError) {
|
|
569
|
+
console.error('Error during failed connect cleanup:', cleanupError);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
463
572
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
|
|
464
573
|
throw error;
|
|
465
574
|
}
|
|
@@ -467,11 +576,14 @@ class SessionManager {
|
|
|
467
576
|
/**
|
|
468
577
|
* Set up event listeners for the Telnyx client
|
|
469
578
|
*/
|
|
470
|
-
_setupClientListeners() {
|
|
471
|
-
if (!
|
|
579
|
+
_setupClientListeners(client) {
|
|
580
|
+
if (!client) {
|
|
472
581
|
return;
|
|
473
582
|
}
|
|
474
|
-
|
|
583
|
+
client.on('telnyx.client.ready', () => {
|
|
584
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
475
587
|
console.log('Telnyx client ready');
|
|
476
588
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTED);
|
|
477
589
|
// Ensure CallStateController listeners are set up when client becomes ready
|
|
@@ -489,7 +601,10 @@ class SessionManager {
|
|
|
489
601
|
console.log('🔧 SessionManager: No _onClientReady callback found in client ready event');
|
|
490
602
|
}
|
|
491
603
|
});
|
|
492
|
-
|
|
604
|
+
client.on('telnyx.client.error', (error) => {
|
|
605
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
493
608
|
console.error('Telnyx client error:', error);
|
|
494
609
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
|
|
495
610
|
});
|
|
@@ -545,5 +660,42 @@ class SessionManager {
|
|
|
545
660
|
_generateSessionId() {
|
|
546
661
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
547
662
|
}
|
|
663
|
+
_assertCanStartConnection() {
|
|
664
|
+
if (this._isTeardownActive()) {
|
|
665
|
+
throw new Error('SessionManager has been disposed');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
_isTeardownActive() {
|
|
669
|
+
return this._disposed || this._disposing;
|
|
670
|
+
}
|
|
671
|
+
_isConnectCanceled(connectionGeneration) {
|
|
672
|
+
return this._isTeardownActive() || this._connectionGeneration !== connectionGeneration;
|
|
673
|
+
}
|
|
674
|
+
_throwIfConnectCanceled(connectionGeneration) {
|
|
675
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
676
|
+
throw this._createConnectCanceledError();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
_createConnectCanceledError() {
|
|
680
|
+
return new Error(
|
|
681
|
+
this._isTeardownActive()
|
|
682
|
+
? 'SessionManager has been disposed'
|
|
683
|
+
: 'SessionManager connection has been canceled'
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
async _disconnectClient(client, errorMessage) {
|
|
687
|
+
try {
|
|
688
|
+
await client.disconnect();
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.error(errorMessage, error);
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async _disconnectAndForgetClient(client, errorMessage) {
|
|
695
|
+
await this._disconnectClient(client, errorMessage);
|
|
696
|
+
if (this._telnyxClient === client) {
|
|
697
|
+
this._telnyxClient = undefined;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
548
700
|
}
|
|
549
701
|
exports.SessionManager = SessionManager;
|
|
@@ -25,6 +25,9 @@ export interface VoicePnBridgeInterface {
|
|
|
25
25
|
): Promise<boolean>;
|
|
26
26
|
hideOngoingCallNotification(): Promise<boolean>;
|
|
27
27
|
hideIncomingCallNotification(): Promise<boolean>;
|
|
28
|
+
setIncomingCallRingtone(resourceName: string | null): Promise<boolean>;
|
|
29
|
+
getPendingCallKitAnswer(): Promise<string | null>;
|
|
30
|
+
clearPendingCallKitAnswer(): Promise<boolean>;
|
|
28
31
|
getVoipToken(): Promise<string | null>;
|
|
29
32
|
getPendingVoipPush(): Promise<string | null>;
|
|
30
33
|
clearPendingVoipPush(): Promise<boolean>;
|
|
@@ -91,6 +94,21 @@ export declare class VoicePnBridge {
|
|
|
91
94
|
* Useful for dismissing notifications when call is answered/rejected in app
|
|
92
95
|
*/
|
|
93
96
|
static hideIncomingCallNotification(): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* Configure Android's incoming-call ringtone using an app resource in `res/raw`.
|
|
99
|
+
* Pass no value to use the device's default phone ringtone.
|
|
100
|
+
*/
|
|
101
|
+
static setIncomingCallRingtone(resourceName?: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
104
|
+
* When the user answers a CallKit call before JS listeners are ready,
|
|
105
|
+
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
106
|
+
*/
|
|
107
|
+
static getPendingCallKitAnswer(): Promise<string | null>;
|
|
108
|
+
/**
|
|
109
|
+
* Clear pending CallKit answer from native storage (iOS only)
|
|
110
|
+
*/
|
|
111
|
+
static clearPendingCallKitAnswer(): Promise<boolean>;
|
|
94
112
|
/**
|
|
95
113
|
* Get VoIP token from native storage
|
|
96
114
|
*/
|
|
@@ -124,6 +124,45 @@ class VoicePnBridge {
|
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Configure Android's incoming-call ringtone using an app resource in `res/raw`.
|
|
129
|
+
* Pass no value to use the device's default phone ringtone.
|
|
130
|
+
*/
|
|
131
|
+
static async setIncomingCallRingtone(resourceName) {
|
|
132
|
+
if (react_native_1.Platform.OS !== 'android') return true;
|
|
133
|
+
try {
|
|
134
|
+
return await NativeBridge.setIncomingCallRingtone(resourceName?.trim() || null);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('VoicePnBridge: Error setting incoming call ringtone:', error);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
142
|
+
* When the user answers a CallKit call before JS listeners are ready,
|
|
143
|
+
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
144
|
+
*/
|
|
145
|
+
static async getPendingCallKitAnswer() {
|
|
146
|
+
if (react_native_1.Platform.OS !== 'ios') return null;
|
|
147
|
+
try {
|
|
148
|
+
return await NativeBridge.getPendingCallKitAnswer();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Clear pending CallKit answer from native storage (iOS only)
|
|
156
|
+
*/
|
|
157
|
+
static async clearPendingCallKitAnswer() {
|
|
158
|
+
if (react_native_1.Platform.OS !== 'ios') return true;
|
|
159
|
+
try {
|
|
160
|
+
return await NativeBridge.clearPendingCallKitAnswer();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
127
166
|
/**
|
|
128
167
|
* Get VoIP token from native storage
|
|
129
168
|
*/
|
package/lib/models/call.js
CHANGED
|
@@ -314,6 +314,18 @@ class Call {
|
|
|
314
314
|
throw new Error(`Cannot hold call in state: ${this.currentState}`);
|
|
315
315
|
}
|
|
316
316
|
try {
|
|
317
|
+
if (react_native_1.Platform.OS === 'ios') {
|
|
318
|
+
const { callKitCoordinator } = await Promise.resolve().then(() =>
|
|
319
|
+
__importStar(require('../callkit/callkit-coordinator'))
|
|
320
|
+
);
|
|
321
|
+
if (callKitCoordinator.isAvailable()) {
|
|
322
|
+
const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, true);
|
|
323
|
+
if (!success) {
|
|
324
|
+
throw new Error('CallKit failed to hold call');
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
317
329
|
await this._telnyxCall.hold();
|
|
318
330
|
} catch (error) {
|
|
319
331
|
console.error('Failed to hold call:', error);
|
|
@@ -328,6 +340,18 @@ class Call {
|
|
|
328
340
|
throw new Error(`Cannot resume call in state: ${this.currentState}`);
|
|
329
341
|
}
|
|
330
342
|
try {
|
|
343
|
+
if (react_native_1.Platform.OS === 'ios') {
|
|
344
|
+
const { callKitCoordinator } = await Promise.resolve().then(() =>
|
|
345
|
+
__importStar(require('../callkit/callkit-coordinator'))
|
|
346
|
+
);
|
|
347
|
+
if (callKitCoordinator.isAvailable()) {
|
|
348
|
+
const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, false);
|
|
349
|
+
if (!success) {
|
|
350
|
+
throw new Error('CallKit failed to resume call');
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
331
355
|
await this._telnyxCall.unhold();
|
|
332
356
|
} catch (error) {
|
|
333
357
|
console.error('Failed to resume call:', error);
|
|
@@ -422,6 +446,17 @@ class Call {
|
|
|
422
446
|
this._telnyxCall.on('telnyx.call.state', (call, state) => {
|
|
423
447
|
const telnyxState = this._mapToTelnyxCallState(state);
|
|
424
448
|
this._callState.next(telnyxState);
|
|
449
|
+
// Keep the dedicated held observable derived from call state. Updating
|
|
450
|
+
// this only after the low-level SDK emits its successful transition
|
|
451
|
+
// preserves the previous value when hold/unhold signaling fails.
|
|
452
|
+
if (telnyxState === call_state_1.TelnyxCallState.HELD) {
|
|
453
|
+
this._isHeld.next(true);
|
|
454
|
+
} else if (
|
|
455
|
+
telnyxState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
456
|
+
call_state_1.CallStateHelpers.isTerminated(telnyxState)
|
|
457
|
+
) {
|
|
458
|
+
this._isHeld.next(false);
|
|
459
|
+
}
|
|
425
460
|
// Start duration timer when call becomes active
|
|
426
461
|
if (telnyxState === call_state_1.TelnyxCallState.ACTIVE && !this._startTime) {
|
|
427
462
|
this._startDurationTimer();
|
package/lib/models/config.d.ts
CHANGED
|
@@ -7,8 +7,12 @@ export interface CredentialConfig {
|
|
|
7
7
|
sipPassword: string;
|
|
8
8
|
debug?: boolean;
|
|
9
9
|
pushNotificationDeviceToken?: string;
|
|
10
|
+
/** Receive PushKit calls while the WebSocket session is active. Default: false */
|
|
11
|
+
pushWhenActive?: boolean;
|
|
10
12
|
/** Enable native missed call push notifications. Default: false */
|
|
11
13
|
enableMissedCallNotifications?: boolean;
|
|
14
|
+
/** Android only: name of a bundled `res/raw` ringtone (without its file extension). Falls back to the device ringtone. */
|
|
15
|
+
incomingCallRingtone?: string;
|
|
12
16
|
/** Enable Trickle ICE. Default: false */
|
|
13
17
|
useTrickleIce?: boolean;
|
|
14
18
|
/** Enable automatic call quality reporting. Default: true */
|
|
@@ -28,8 +32,12 @@ export interface TokenConfig {
|
|
|
28
32
|
token: string;
|
|
29
33
|
debug?: boolean;
|
|
30
34
|
pushNotificationDeviceToken?: string;
|
|
35
|
+
/** Receive PushKit calls while the WebSocket session is active. Default: false */
|
|
36
|
+
pushWhenActive?: boolean;
|
|
31
37
|
/** Enable native missed call push notifications. Default: false */
|
|
32
38
|
enableMissedCallNotifications?: boolean;
|
|
39
|
+
/** Android only: name of a bundled `res/raw` ringtone (without its file extension). Falls back to the device ringtone. */
|
|
40
|
+
incomingCallRingtone?: string;
|
|
33
41
|
/** Enable Trickle ICE. Default: false */
|
|
34
42
|
useTrickleIce?: boolean;
|
|
35
43
|
/** Enable automatic call quality reporting. Default: true */
|
|
@@ -73,6 +81,7 @@ export declare function validateConfig(config: Config): string[];
|
|
|
73
81
|
* @param options - Optional configuration settings
|
|
74
82
|
* @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
|
|
75
83
|
* @param options.pushNotificationDeviceToken - Device token for push notifications
|
|
84
|
+
* @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
|
|
76
85
|
* @returns Complete credential configuration object
|
|
77
86
|
*/
|
|
78
87
|
export declare function createCredentialConfig(
|
|
@@ -87,6 +96,7 @@ export declare function createCredentialConfig(
|
|
|
87
96
|
* @param options - Optional configuration settings
|
|
88
97
|
* @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
|
|
89
98
|
* @param options.pushNotificationDeviceToken - Device token for push notifications
|
|
99
|
+
* @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
|
|
90
100
|
* @returns Complete token configuration object
|
|
91
101
|
*/
|
|
92
102
|
export declare function createTokenConfig(
|
package/lib/models/config.js
CHANGED
|
@@ -62,6 +62,7 @@ function validateConfig(config) {
|
|
|
62
62
|
* @param options - Optional configuration settings
|
|
63
63
|
* @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
|
|
64
64
|
* @param options.pushNotificationDeviceToken - Device token for push notifications
|
|
65
|
+
* @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
|
|
65
66
|
* @returns Complete credential configuration object
|
|
66
67
|
*/
|
|
67
68
|
function createCredentialConfig(sipUser, sipPassword, options) {
|
|
@@ -79,6 +80,7 @@ function createCredentialConfig(sipUser, sipPassword, options) {
|
|
|
79
80
|
* @param options - Optional configuration settings
|
|
80
81
|
* @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
|
|
81
82
|
* @param options.pushNotificationDeviceToken - Device token for push notifications
|
|
83
|
+
* @param options.pushWhenActive - Keep active PushKit devices eligible for incoming calls
|
|
82
84
|
* @returns Complete token configuration object
|
|
83
85
|
*/
|
|
84
86
|
function createTokenConfig(sipToken, options) {
|