@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.
- 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 +20 -1
- package/ios/CallKitBridge.swift +527 -78
- 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 +6 -4
- 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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../../models/config';
|
|
13
13
|
|
|
14
14
|
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
15
|
+
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
15
16
|
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -28,6 +29,10 @@ export class SessionManager {
|
|
|
28
29
|
private _currentConfig?: Config;
|
|
29
30
|
private _sessionId: string;
|
|
30
31
|
private _disposed = false;
|
|
32
|
+
private _disposing = false;
|
|
33
|
+
private _connectionGeneration = 0;
|
|
34
|
+
private _disposePromise?: Promise<void>;
|
|
35
|
+
private _connectPromise?: Promise<void>;
|
|
31
36
|
private _onClientReady?: () => void;
|
|
32
37
|
private _onDisconnect?: () => void;
|
|
33
38
|
|
|
@@ -86,24 +91,20 @@ export class SessionManager {
|
|
|
86
91
|
* Connect using credential authentication
|
|
87
92
|
*/
|
|
88
93
|
async connectWithCredential(config: CredentialConfig): Promise<void> {
|
|
89
|
-
|
|
90
|
-
throw new Error('SessionManager has been disposed');
|
|
91
|
-
}
|
|
92
|
-
|
|
94
|
+
this._assertCanStartConnection();
|
|
93
95
|
this._currentConfig = config;
|
|
94
96
|
await this._connect();
|
|
97
|
+
this._assertCanStartConnection();
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/**
|
|
98
101
|
* Connect using token authentication
|
|
99
102
|
*/
|
|
100
103
|
async connectWithToken(config: TokenConfig): Promise<void> {
|
|
101
|
-
|
|
102
|
-
throw new Error('SessionManager has been disposed');
|
|
103
|
-
}
|
|
104
|
-
|
|
104
|
+
this._assertCanStartConnection();
|
|
105
105
|
this._currentConfig = config;
|
|
106
106
|
await this._connect();
|
|
107
|
+
this._assertCanStartConnection();
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
@@ -121,6 +122,7 @@ export class SessionManager {
|
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
this._connectionGeneration += 1;
|
|
124
126
|
this._currentConfig = undefined;
|
|
125
127
|
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
126
128
|
|
|
@@ -133,11 +135,7 @@ export class SessionManager {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
if (this._telnyxClient) {
|
|
136
|
-
|
|
137
|
-
await this._telnyxClient.disconnect();
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('Error during disconnect:', error);
|
|
140
|
-
}
|
|
138
|
+
await this._disconnectAndForgetClient(this._telnyxClient, 'Error during disconnect:');
|
|
141
139
|
}
|
|
142
140
|
}
|
|
143
141
|
|
|
@@ -147,7 +145,11 @@ export class SessionManager {
|
|
|
147
145
|
* which sends a 'telnyx_rtc.disable_push_notification' message via the socket.
|
|
148
146
|
*/
|
|
149
147
|
disablePushNotifications(): void {
|
|
150
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
!this._isTeardownActive() &&
|
|
150
|
+
this._telnyxClient &&
|
|
151
|
+
this.currentState === TelnyxConnectionState.CONNECTED
|
|
152
|
+
) {
|
|
151
153
|
console.log('SessionManager: Disabling push notifications for session:', this._sessionId);
|
|
152
154
|
this._telnyxClient.disablePushNotification();
|
|
153
155
|
} else {
|
|
@@ -159,7 +161,7 @@ export class SessionManager {
|
|
|
159
161
|
* Handle push notification with stored config
|
|
160
162
|
*/
|
|
161
163
|
handlePushNotificationWithConfig(pushMetaData: any, config: Config): void {
|
|
162
|
-
if (this.
|
|
164
|
+
if (this._isTeardownActive()) {
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -173,7 +175,7 @@ export class SessionManager {
|
|
|
173
175
|
* Handle push notification (async version)
|
|
174
176
|
*/
|
|
175
177
|
async handlePushNotification(payload: Record<string, any>): Promise<void> {
|
|
176
|
-
if (this.
|
|
178
|
+
if (this._isTeardownActive()) {
|
|
177
179
|
return;
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -185,19 +187,38 @@ export class SessionManager {
|
|
|
185
187
|
// Store the push notification payload for when the client is created
|
|
186
188
|
(this as any)._pendingPushPayload = payload;
|
|
187
189
|
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
console.warn('SessionManager: disconnect of prior client threw:', err);
|
|
190
|
+
// A second-call push must stay on the client that owns the active/held
|
|
191
|
+
// call. Tearing it down destroys the first signaling session and clears
|
|
192
|
+
// the call collection before CallKit can complete Hold & Accept.
|
|
193
|
+
if (this._telnyxClient && this._hasActiveOrHeldCall(this._telnyxClient)) {
|
|
194
|
+
const actualPayload = this._extractPushPayload(payload);
|
|
195
|
+
const processVoIPNotification = (this._telnyxClient as any).processVoIPNotification;
|
|
196
|
+
|
|
197
|
+
if (typeof processVoIPNotification !== 'function') {
|
|
198
|
+
throw new Error('TelnyxRTC client cannot process an active-call VoIP notification');
|
|
198
199
|
}
|
|
200
|
+
|
|
201
|
+
console.log(
|
|
202
|
+
'SessionManager: Preserving active client while processing second-call push notification'
|
|
203
|
+
);
|
|
204
|
+
processVoIPNotification.call(this._telnyxClient, actualPayload);
|
|
205
|
+
(this as any)._pendingPushPayload = null;
|
|
206
|
+
return;
|
|
199
207
|
}
|
|
200
|
-
|
|
208
|
+
|
|
209
|
+
// The cold/no-active-call path still rebuilds the client so the socket is
|
|
210
|
+
// stamped with the push voice_sdk_id before connecting.
|
|
211
|
+
if (this._telnyxClient) {
|
|
212
|
+
await this._disconnectAndForgetClient(
|
|
213
|
+
this._telnyxClient,
|
|
214
|
+
'SessionManager: disconnect of prior client threw:'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this._isTeardownActive()) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
201
222
|
if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
|
|
202
223
|
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
203
224
|
}
|
|
@@ -218,12 +239,18 @@ export class SessionManager {
|
|
|
218
239
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
219
240
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
220
241
|
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
242
|
+
const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
|
|
221
243
|
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
222
244
|
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
223
245
|
);
|
|
224
246
|
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
247
|
+
const pushWhenActive = storedPushWhenActive === 'true';
|
|
225
248
|
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
226
249
|
|
|
250
|
+
if (this._isTeardownActive()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
227
254
|
// Check if we have credential-based authentication data
|
|
228
255
|
if (storedUsername && storedPassword) {
|
|
229
256
|
console.log('SessionManager: RELEASE DEBUG - Found stored credentials, creating config');
|
|
@@ -231,6 +258,7 @@ export class SessionManager {
|
|
|
231
258
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
232
259
|
pushNotificationDeviceToken: storedPushToken,
|
|
233
260
|
useTrickleIce,
|
|
261
|
+
pushWhenActive,
|
|
234
262
|
enableMissedCallNotifications,
|
|
235
263
|
});
|
|
236
264
|
}
|
|
@@ -241,6 +269,7 @@ export class SessionManager {
|
|
|
241
269
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
242
270
|
pushNotificationDeviceToken: storedPushToken,
|
|
243
271
|
useTrickleIce,
|
|
272
|
+
pushWhenActive,
|
|
244
273
|
enableMissedCallNotifications,
|
|
245
274
|
});
|
|
246
275
|
}
|
|
@@ -257,6 +286,10 @@ export class SessionManager {
|
|
|
257
286
|
}
|
|
258
287
|
}
|
|
259
288
|
|
|
289
|
+
if (this._isTeardownActive()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
260
293
|
// If we already have a client, process the push notification immediately
|
|
261
294
|
if (this._telnyxClient) {
|
|
262
295
|
console.log(
|
|
@@ -306,10 +339,16 @@ export class SessionManager {
|
|
|
306
339
|
);
|
|
307
340
|
try {
|
|
308
341
|
await this._connect();
|
|
342
|
+
if (this._isTeardownActive()) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
309
345
|
console.log(
|
|
310
346
|
'SessionManager: RELEASE DEBUG - Successfully connected after push notification trigger'
|
|
311
347
|
);
|
|
312
348
|
} catch (error) {
|
|
349
|
+
if (this._isTeardownActive()) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
313
352
|
console.error(
|
|
314
353
|
'SessionManager: Failed to connect after push notification trigger:',
|
|
315
354
|
error
|
|
@@ -331,84 +370,160 @@ export class SessionManager {
|
|
|
331
370
|
console.log('SessionManager: RELEASE DEBUG - Push notification handling complete');
|
|
332
371
|
}
|
|
333
372
|
|
|
373
|
+
private _hasActiveOrHeldCall(client: TelnyxSDK.TelnyxRTC): boolean {
|
|
374
|
+
const calls =
|
|
375
|
+
typeof (client as any).getActiveCalls === 'function'
|
|
376
|
+
? (client as any).getActiveCalls()
|
|
377
|
+
: Array.from(((client as any).calls as Map<string, any> | undefined)?.values?.() || []);
|
|
378
|
+
|
|
379
|
+
return calls.some((call: any) => call?.state === 'active' || call?.state === 'held');
|
|
380
|
+
}
|
|
381
|
+
|
|
334
382
|
/**
|
|
335
383
|
* Dispose of the session manager and clean up resources
|
|
336
384
|
*/
|
|
337
|
-
dispose(): void {
|
|
385
|
+
async dispose(): Promise<void> {
|
|
338
386
|
if (this._disposed) {
|
|
339
387
|
return;
|
|
340
388
|
}
|
|
341
389
|
|
|
342
|
-
this.
|
|
343
|
-
|
|
344
|
-
|
|
390
|
+
if (!this._disposePromise) {
|
|
391
|
+
this._disposePromise = this._dispose();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await this._disposePromise;
|
|
345
395
|
}
|
|
346
396
|
|
|
347
397
|
/**
|
|
348
398
|
* Internal method to establish connection with or without push notification handling
|
|
349
399
|
*/
|
|
350
400
|
private async _connect(): Promise<void> {
|
|
351
|
-
|
|
401
|
+
const previousConnect = this._connectPromise;
|
|
402
|
+
if (previousConnect) {
|
|
403
|
+
try {
|
|
404
|
+
await previousConnect;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (this._isTeardownActive()) {
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this._assertCanStartConnection();
|
|
413
|
+
|
|
414
|
+
const config = this._currentConfig;
|
|
415
|
+
if (!config) {
|
|
352
416
|
throw new Error('No configuration provided');
|
|
353
417
|
}
|
|
354
418
|
|
|
419
|
+
const connectionGeneration = (this._connectionGeneration += 1);
|
|
420
|
+
const connectPromise = this._runConnect(config, connectionGeneration);
|
|
421
|
+
this._connectPromise = connectPromise;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await connectPromise;
|
|
425
|
+
} finally {
|
|
426
|
+
if (this._connectPromise === connectPromise) {
|
|
427
|
+
this._connectPromise = undefined;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private async _dispose(): Promise<void> {
|
|
433
|
+
this._disposing = true;
|
|
434
|
+
this._connectionGeneration += 1;
|
|
435
|
+
|
|
436
|
+
const inFlightConnect = this._connectPromise;
|
|
437
|
+
|
|
438
|
+
await this.disconnect();
|
|
439
|
+
|
|
440
|
+
if (inFlightConnect) {
|
|
441
|
+
try {
|
|
442
|
+
await inFlightConnect;
|
|
443
|
+
} catch {
|
|
444
|
+
// A connect canceled by disposal is expected; the cleanup path runs in _runConnect.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const client = this._telnyxClient;
|
|
449
|
+
if (client) {
|
|
450
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this._currentConfig = undefined;
|
|
454
|
+
(this as any)._pendingPushPayload = null;
|
|
455
|
+
this._disposed = true;
|
|
456
|
+
this._disposing = false;
|
|
457
|
+
this._connectionState.complete();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async _runConnect(config: Config, connectionGeneration: number): Promise<void> {
|
|
461
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
355
462
|
this._connectionState.next(TelnyxConnectionState.CONNECTING);
|
|
356
463
|
|
|
464
|
+
let client: TelnyxSDK.TelnyxRTC | undefined;
|
|
465
|
+
let canceledConnectCleanupCompleted = false;
|
|
466
|
+
|
|
357
467
|
try {
|
|
358
468
|
// Clean up existing client
|
|
359
469
|
if (this._telnyxClient) {
|
|
360
|
-
await this._telnyxClient
|
|
470
|
+
await this._disconnectClient(this._telnyxClient, 'Error during disconnect:');
|
|
361
471
|
}
|
|
362
472
|
|
|
473
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
474
|
+
|
|
363
475
|
// Create new client instance with authentication options
|
|
364
476
|
let clientOptions: TelnyxSDK.ClientOptions;
|
|
365
477
|
|
|
366
|
-
if (isCredentialConfig(
|
|
478
|
+
if (isCredentialConfig(config)) {
|
|
367
479
|
clientOptions = {
|
|
368
|
-
login:
|
|
369
|
-
password:
|
|
370
|
-
logLevel:
|
|
371
|
-
debug:
|
|
372
|
-
pushNotificationDeviceToken:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
480
|
+
login: config.sipUser,
|
|
481
|
+
password: config.sipPassword,
|
|
482
|
+
logLevel: config.debug ? 'debug' : 'warn',
|
|
483
|
+
debug: config.debug ?? false,
|
|
484
|
+
pushNotificationDeviceToken: config.pushNotificationDeviceToken,
|
|
485
|
+
pushWhenActive: config.pushWhenActive,
|
|
486
|
+
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
487
|
+
useTrickleIce: config.useTrickleIce,
|
|
488
|
+
enableCallReports: config.enableCallReports,
|
|
489
|
+
callReportInterval: config.callReportInterval,
|
|
490
|
+
callReportLogLevel: config.callReportLogLevel,
|
|
491
|
+
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
379
492
|
sdkVersion: pkg.version,
|
|
380
493
|
};
|
|
381
494
|
console.log(
|
|
382
495
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
383
496
|
clientOptions.logLevel,
|
|
384
497
|
'pushToken:',
|
|
385
|
-
!!
|
|
498
|
+
!!config.pushNotificationDeviceToken
|
|
386
499
|
);
|
|
387
|
-
} else if (isTokenConfig(
|
|
500
|
+
} else if (isTokenConfig(config)) {
|
|
388
501
|
clientOptions = {
|
|
389
|
-
login_token:
|
|
390
|
-
logLevel:
|
|
391
|
-
debug:
|
|
392
|
-
pushNotificationDeviceToken:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
502
|
+
login_token: config.token,
|
|
503
|
+
logLevel: config.debug ? 'debug' : 'warn',
|
|
504
|
+
debug: config.debug ?? false,
|
|
505
|
+
pushNotificationDeviceToken: config.pushNotificationDeviceToken,
|
|
506
|
+
pushWhenActive: config.pushWhenActive,
|
|
507
|
+
enableMissedCallNotifications: config.enableMissedCallNotifications ?? false,
|
|
508
|
+
useTrickleIce: config.useTrickleIce,
|
|
509
|
+
enableCallReports: config.enableCallReports,
|
|
510
|
+
callReportInterval: config.callReportInterval,
|
|
511
|
+
callReportLogLevel: config.callReportLogLevel,
|
|
512
|
+
callReportMaxLogEntries: config.callReportMaxLogEntries,
|
|
399
513
|
sdkVersion: pkg.version,
|
|
400
514
|
};
|
|
401
515
|
console.log(
|
|
402
516
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
403
517
|
clientOptions.logLevel,
|
|
404
518
|
'pushToken:',
|
|
405
|
-
!!
|
|
519
|
+
!!config.pushNotificationDeviceToken
|
|
406
520
|
);
|
|
407
521
|
} else {
|
|
408
522
|
throw new Error('Invalid configuration type');
|
|
409
523
|
}
|
|
410
524
|
|
|
411
|
-
|
|
525
|
+
client = new TelnyxSDK.TelnyxRTC(clientOptions);
|
|
526
|
+
this._telnyxClient = client;
|
|
412
527
|
|
|
413
528
|
// CRITICAL: Process any pending push notification payload BEFORE connecting
|
|
414
529
|
// This ensures voice_sdk_id and other payload variables are set before connect() is called
|
|
@@ -441,7 +556,7 @@ export class SessionManager {
|
|
|
441
556
|
(this as any)._pendingPushPayload = null;
|
|
442
557
|
}
|
|
443
558
|
|
|
444
|
-
this._setupClientListeners();
|
|
559
|
+
this._setupClientListeners(client);
|
|
445
560
|
|
|
446
561
|
// Set up CallStateController listeners immediately after client creation
|
|
447
562
|
// This ensures they're ready before any incoming call events are emitted
|
|
@@ -457,16 +572,38 @@ export class SessionManager {
|
|
|
457
572
|
console.log('🔧 SessionManager: No _onClientReady callback found');
|
|
458
573
|
}
|
|
459
574
|
|
|
575
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
576
|
+
|
|
460
577
|
// Connect to the platform AFTER processing push notification
|
|
461
578
|
console.log(
|
|
462
579
|
'SessionManager: RELEASE DEBUG - About to call connect() after processing push notification'
|
|
463
580
|
);
|
|
464
|
-
await
|
|
581
|
+
await client.connect();
|
|
582
|
+
|
|
583
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
584
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
585
|
+
canceledConnectCleanupCompleted = true;
|
|
586
|
+
throw this._createConnectCanceledError();
|
|
587
|
+
}
|
|
465
588
|
|
|
466
589
|
// Notify that client is ready for event listeners
|
|
467
590
|
console.log('🔧 SessionManager: Client connected successfully');
|
|
468
591
|
} catch (error) {
|
|
592
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
593
|
+
if (client && !canceledConnectCleanupCompleted) {
|
|
594
|
+
await this._disconnectAndForgetClient(client, 'Error during dispose disconnect:');
|
|
595
|
+
}
|
|
596
|
+
throw this._createConnectCanceledError();
|
|
597
|
+
}
|
|
598
|
+
|
|
469
599
|
console.error('Connection failed:', error);
|
|
600
|
+
if (client) {
|
|
601
|
+
try {
|
|
602
|
+
await this._disconnectAndForgetClient(client, 'Error during failed connect cleanup:');
|
|
603
|
+
} catch (cleanupError) {
|
|
604
|
+
console.error('Error during failed connect cleanup:', cleanupError);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
470
607
|
this._connectionState.next(TelnyxConnectionState.ERROR);
|
|
471
608
|
throw error;
|
|
472
609
|
}
|
|
@@ -475,12 +612,16 @@ export class SessionManager {
|
|
|
475
612
|
/**
|
|
476
613
|
* Set up event listeners for the Telnyx client
|
|
477
614
|
*/
|
|
478
|
-
private _setupClientListeners(): void {
|
|
479
|
-
if (!
|
|
615
|
+
private _setupClientListeners(client: TelnyxSDK.TelnyxRTC): void {
|
|
616
|
+
if (!client) {
|
|
480
617
|
return;
|
|
481
618
|
}
|
|
482
619
|
|
|
483
|
-
|
|
620
|
+
client.on('telnyx.client.ready', () => {
|
|
621
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
484
625
|
console.log('Telnyx client ready');
|
|
485
626
|
this._connectionState.next(TelnyxConnectionState.CONNECTED);
|
|
486
627
|
|
|
@@ -500,7 +641,11 @@ export class SessionManager {
|
|
|
500
641
|
}
|
|
501
642
|
});
|
|
502
643
|
|
|
503
|
-
|
|
644
|
+
client.on('telnyx.client.error', (error: Error) => {
|
|
645
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
504
649
|
console.error('Telnyx client error:', error);
|
|
505
650
|
this._connectionState.next(TelnyxConnectionState.ERROR);
|
|
506
651
|
});
|
|
@@ -565,4 +710,54 @@ export class SessionManager {
|
|
|
565
710
|
private _generateSessionId(): string {
|
|
566
711
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
567
712
|
}
|
|
713
|
+
|
|
714
|
+
private _assertCanStartConnection(): void {
|
|
715
|
+
if (this._isTeardownActive()) {
|
|
716
|
+
throw new Error('SessionManager has been disposed');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private _isTeardownActive(): boolean {
|
|
721
|
+
return this._disposed || this._disposing;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private _isConnectCanceled(connectionGeneration: number): boolean {
|
|
725
|
+
return this._isTeardownActive() || this._connectionGeneration !== connectionGeneration;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private _throwIfConnectCanceled(connectionGeneration: number): void {
|
|
729
|
+
if (this._isConnectCanceled(connectionGeneration)) {
|
|
730
|
+
throw this._createConnectCanceledError();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private _createConnectCanceledError(): Error {
|
|
735
|
+
return new Error(
|
|
736
|
+
this._isTeardownActive()
|
|
737
|
+
? 'SessionManager has been disposed'
|
|
738
|
+
: 'SessionManager connection has been canceled'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private async _disconnectClient(
|
|
743
|
+
client: TelnyxSDK.TelnyxRTC,
|
|
744
|
+
errorMessage: string
|
|
745
|
+
): Promise<void> {
|
|
746
|
+
try {
|
|
747
|
+
await client.disconnect();
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error(errorMessage, error);
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private async _disconnectAndForgetClient(
|
|
755
|
+
client: TelnyxSDK.TelnyxRTC,
|
|
756
|
+
errorMessage: string
|
|
757
|
+
): Promise<void> {
|
|
758
|
+
await this._disconnectClient(client, errorMessage);
|
|
759
|
+
if (this._telnyxClient === client) {
|
|
760
|
+
this._telnyxClient = undefined;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
568
763
|
}
|
|
@@ -31,6 +31,11 @@ export interface VoicePnBridgeInterface {
|
|
|
31
31
|
): Promise<boolean>;
|
|
32
32
|
hideOngoingCallNotification(): Promise<boolean>;
|
|
33
33
|
hideIncomingCallNotification(): Promise<boolean>;
|
|
34
|
+
setIncomingCallRingtone(resourceName: string | null): Promise<boolean>;
|
|
35
|
+
|
|
36
|
+
// CallKit answer persistence (iOS only)
|
|
37
|
+
getPendingCallKitAnswer(): Promise<string | null>;
|
|
38
|
+
clearPendingCallKitAnswer(): Promise<boolean>;
|
|
34
39
|
|
|
35
40
|
// Additional UserDefaults methods
|
|
36
41
|
getVoipToken(): Promise<string | null>;
|
|
@@ -183,6 +188,48 @@ export class VoicePnBridge {
|
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Configure Android's incoming-call ringtone using an app resource in `res/raw`.
|
|
193
|
+
* Pass no value to use the device's default phone ringtone.
|
|
194
|
+
*/
|
|
195
|
+
static async setIncomingCallRingtone(resourceName?: string): Promise<boolean> {
|
|
196
|
+
if (Platform.OS !== 'android') return true;
|
|
197
|
+
try {
|
|
198
|
+
return await NativeBridge.setIncomingCallRingtone(resourceName?.trim() || null);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('VoicePnBridge: Error setting incoming call ringtone:', error);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
207
|
+
* When the user answers a CallKit call before JS listeners are ready,
|
|
208
|
+
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
209
|
+
*/
|
|
210
|
+
static async getPendingCallKitAnswer(): Promise<string | null> {
|
|
211
|
+
if (Platform.OS !== 'ios') return null;
|
|
212
|
+
try {
|
|
213
|
+
return await NativeBridge.getPendingCallKitAnswer();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clear pending CallKit answer from native storage (iOS only)
|
|
222
|
+
*/
|
|
223
|
+
static async clearPendingCallKitAnswer(): Promise<boolean> {
|
|
224
|
+
if (Platform.OS !== 'ios') return true;
|
|
225
|
+
try {
|
|
226
|
+
return await NativeBridge.clearPendingCallKitAnswer();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
186
233
|
/**
|
|
187
234
|
* Get VoIP token from native storage
|
|
188
235
|
*/
|
package/src/models/call.ts
CHANGED
|
@@ -285,6 +285,17 @@ export class Call {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
try {
|
|
288
|
+
if (Platform.OS === 'ios') {
|
|
289
|
+
const { callKitCoordinator } = await import('../callkit/callkit-coordinator');
|
|
290
|
+
if (callKitCoordinator.isAvailable()) {
|
|
291
|
+
const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, true);
|
|
292
|
+
if (!success) {
|
|
293
|
+
throw new Error('CallKit failed to hold call');
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
await this._telnyxCall.hold();
|
|
289
300
|
} catch (error) {
|
|
290
301
|
console.error('Failed to hold call:', error);
|
|
@@ -301,6 +312,17 @@ export class Call {
|
|
|
301
312
|
}
|
|
302
313
|
|
|
303
314
|
try {
|
|
315
|
+
if (Platform.OS === 'ios') {
|
|
316
|
+
const { callKitCoordinator } = await import('../callkit/callkit-coordinator');
|
|
317
|
+
if (callKitCoordinator.isAvailable()) {
|
|
318
|
+
const success = await callKitCoordinator.setHeldFromUI(this._telnyxCall, false);
|
|
319
|
+
if (!success) {
|
|
320
|
+
throw new Error('CallKit failed to resume call');
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
304
326
|
await this._telnyxCall.unhold();
|
|
305
327
|
} catch (error) {
|
|
306
328
|
console.error('Failed to resume call:', error);
|
|
@@ -406,6 +428,18 @@ export class Call {
|
|
|
406
428
|
const telnyxState = this._mapToTelnyxCallState(state);
|
|
407
429
|
this._callState.next(telnyxState);
|
|
408
430
|
|
|
431
|
+
// Keep the dedicated held observable derived from call state. Updating
|
|
432
|
+
// this only after the low-level SDK emits its successful transition
|
|
433
|
+
// preserves the previous value when hold/unhold signaling fails.
|
|
434
|
+
if (telnyxState === TelnyxCallState.HELD) {
|
|
435
|
+
this._isHeld.next(true);
|
|
436
|
+
} else if (
|
|
437
|
+
telnyxState === TelnyxCallState.ACTIVE ||
|
|
438
|
+
CallStateHelpers.isTerminated(telnyxState)
|
|
439
|
+
) {
|
|
440
|
+
this._isHeld.next(false);
|
|
441
|
+
}
|
|
442
|
+
|
|
409
443
|
// Start duration timer when call becomes active
|
|
410
444
|
if (telnyxState === TelnyxCallState.ACTIVE && !this._startTime) {
|
|
411
445
|
this._startDurationTimer();
|