@telnyx/react-voice-commons-sdk 0.4.3 → 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/TelnyxFirebaseMessagingService.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 +74 -1
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +935 -60
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +21 -1
- package/ios/VoicePnBridge.swift +62 -1
- 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/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +39 -1
- package/lib/internal/calls/call-state-controller.js +152 -64
- package/lib/internal/session/session-manager.d.ts +16 -1
- package/lib/internal/session/session-manager.js +229 -58
- package/lib/internal/voice-pn-bridge.d.ts +44 -0
- package/lib/internal/voice-pn-bridge.js +94 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +18 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +48 -5
- package/lib/telnyx-voip-client.js +276 -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/index.ts +3 -0
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +173 -74
- package/src/internal/session/session-manager.ts +277 -61
- package/src/internal/voice-pn-bridge.ts +111 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +18 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +260 -24
- package/src/types/telnyx-sdk.d.ts +12 -1
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
isTokenConfig,
|
|
12
12
|
} from '../../models/config';
|
|
13
13
|
|
|
14
|
+
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
15
|
+
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
16
|
+
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
16
20
|
*
|
|
@@ -25,6 +29,10 @@ export class SessionManager {
|
|
|
25
29
|
private _currentConfig?: Config;
|
|
26
30
|
private _sessionId: string;
|
|
27
31
|
private _disposed = false;
|
|
32
|
+
private _disposing = false;
|
|
33
|
+
private _connectionGeneration = 0;
|
|
34
|
+
private _disposePromise?: Promise<void>;
|
|
35
|
+
private _connectPromise?: Promise<void>;
|
|
28
36
|
private _onClientReady?: () => void;
|
|
29
37
|
private _onDisconnect?: () => void;
|
|
30
38
|
|
|
@@ -75,28 +83,28 @@ export class SessionManager {
|
|
|
75
83
|
return this._telnyxClient;
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
get useTrickleIce(): boolean {
|
|
87
|
+
return Boolean(this._currentConfig?.useTrickleIce);
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
/**
|
|
79
91
|
* Connect using credential authentication
|
|
80
92
|
*/
|
|
81
93
|
async connectWithCredential(config: CredentialConfig): Promise<void> {
|
|
82
|
-
|
|
83
|
-
throw new Error('SessionManager has been disposed');
|
|
84
|
-
}
|
|
85
|
-
|
|
94
|
+
this._assertCanStartConnection();
|
|
86
95
|
this._currentConfig = config;
|
|
87
96
|
await this._connect();
|
|
97
|
+
this._assertCanStartConnection();
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/**
|
|
91
101
|
* Connect using token authentication
|
|
92
102
|
*/
|
|
93
103
|
async connectWithToken(config: TokenConfig): Promise<void> {
|
|
94
|
-
|
|
95
|
-
throw new Error('SessionManager has been disposed');
|
|
96
|
-
}
|
|
97
|
-
|
|
104
|
+
this._assertCanStartConnection();
|
|
98
105
|
this._currentConfig = config;
|
|
99
106
|
await this._connect();
|
|
107
|
+
this._assertCanStartConnection();
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
/**
|
|
@@ -114,6 +122,7 @@ export class SessionManager {
|
|
|
114
122
|
return;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
this._connectionGeneration += 1;
|
|
117
126
|
this._currentConfig = undefined;
|
|
118
127
|
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
119
128
|
|
|
@@ -126,11 +135,7 @@ export class SessionManager {
|
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
if (this._telnyxClient) {
|
|
129
|
-
|
|
130
|
-
await this._telnyxClient.disconnect();
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error('Error during disconnect:', error);
|
|
133
|
-
}
|
|
138
|
+
await this._disconnectAndForgetClient(this._telnyxClient, 'Error during disconnect:');
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -140,7 +145,11 @@ export class SessionManager {
|
|
|
140
145
|
* which sends a 'telnyx_rtc.disable_push_notification' message via the socket.
|
|
141
146
|
*/
|
|
142
147
|
disablePushNotifications(): void {
|
|
143
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
!this._isTeardownActive() &&
|
|
150
|
+
this._telnyxClient &&
|
|
151
|
+
this.currentState === TelnyxConnectionState.CONNECTED
|
|
152
|
+
) {
|
|
144
153
|
console.log('SessionManager: Disabling push notifications for session:', this._sessionId);
|
|
145
154
|
this._telnyxClient.disablePushNotification();
|
|
146
155
|
} else {
|
|
@@ -152,7 +161,7 @@ export class SessionManager {
|
|
|
152
161
|
* Handle push notification with stored config
|
|
153
162
|
*/
|
|
154
163
|
handlePushNotificationWithConfig(pushMetaData: any, config: Config): void {
|
|
155
|
-
if (this.
|
|
164
|
+
if (this._isTeardownActive()) {
|
|
156
165
|
return;
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -166,7 +175,7 @@ export class SessionManager {
|
|
|
166
175
|
* Handle push notification (async version)
|
|
167
176
|
*/
|
|
168
177
|
async handlePushNotification(payload: Record<string, any>): Promise<void> {
|
|
169
|
-
if (this.
|
|
178
|
+
if (this._isTeardownActive()) {
|
|
170
179
|
return;
|
|
171
180
|
}
|
|
172
181
|
|
|
@@ -178,19 +187,38 @@ export class SessionManager {
|
|
|
178
187
|
// Store the push notification payload for when the client is created
|
|
179
188
|
(this as any)._pendingPushPayload = payload;
|
|
180
189
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
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');
|
|
191
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;
|
|
207
|
+
}
|
|
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
|
+
);
|
|
192
216
|
}
|
|
193
|
-
|
|
217
|
+
|
|
218
|
+
if (this._isTeardownActive()) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
194
222
|
if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
|
|
195
223
|
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
196
224
|
}
|
|
@@ -210,6 +238,18 @@ export class SessionManager {
|
|
|
210
238
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
211
239
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
212
240
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
241
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
242
|
+
const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
|
|
243
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
244
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
245
|
+
);
|
|
246
|
+
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
247
|
+
const pushWhenActive = storedPushWhenActive === 'true';
|
|
248
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
249
|
+
|
|
250
|
+
if (this._isTeardownActive()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
213
253
|
|
|
214
254
|
// Check if we have credential-based authentication data
|
|
215
255
|
if (storedUsername && storedPassword) {
|
|
@@ -217,6 +257,9 @@ export class SessionManager {
|
|
|
217
257
|
const { createCredentialConfig } = require('../../models/config');
|
|
218
258
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
219
259
|
pushNotificationDeviceToken: storedPushToken,
|
|
260
|
+
useTrickleIce,
|
|
261
|
+
pushWhenActive,
|
|
262
|
+
enableMissedCallNotifications,
|
|
220
263
|
});
|
|
221
264
|
}
|
|
222
265
|
// Check if we have token-based authentication data
|
|
@@ -225,6 +268,9 @@ export class SessionManager {
|
|
|
225
268
|
const { createTokenConfig } = require('../../models/config');
|
|
226
269
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
227
270
|
pushNotificationDeviceToken: storedPushToken,
|
|
271
|
+
useTrickleIce,
|
|
272
|
+
pushWhenActive,
|
|
273
|
+
enableMissedCallNotifications,
|
|
228
274
|
});
|
|
229
275
|
}
|
|
230
276
|
|
|
@@ -240,6 +286,10 @@ export class SessionManager {
|
|
|
240
286
|
}
|
|
241
287
|
}
|
|
242
288
|
|
|
289
|
+
if (this._isTeardownActive()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
243
293
|
// If we already have a client, process the push notification immediately
|
|
244
294
|
if (this._telnyxClient) {
|
|
245
295
|
console.log(
|
|
@@ -289,10 +339,16 @@ export class SessionManager {
|
|
|
289
339
|
);
|
|
290
340
|
try {
|
|
291
341
|
await this._connect();
|
|
342
|
+
if (this._isTeardownActive()) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
292
345
|
console.log(
|
|
293
346
|
'SessionManager: RELEASE DEBUG - Successfully connected after push notification trigger'
|
|
294
347
|
);
|
|
295
348
|
} catch (error) {
|
|
349
|
+
if (this._isTeardownActive()) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
296
352
|
console.error(
|
|
297
353
|
'SessionManager: Failed to connect after push notification trigger:',
|
|
298
354
|
error
|
|
@@ -314,80 +370,160 @@ export class SessionManager {
|
|
|
314
370
|
console.log('SessionManager: RELEASE DEBUG - Push notification handling complete');
|
|
315
371
|
}
|
|
316
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
|
+
|
|
317
382
|
/**
|
|
318
383
|
* Dispose of the session manager and clean up resources
|
|
319
384
|
*/
|
|
320
|
-
dispose(): void {
|
|
385
|
+
async dispose(): Promise<void> {
|
|
321
386
|
if (this._disposed) {
|
|
322
387
|
return;
|
|
323
388
|
}
|
|
324
389
|
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
390
|
+
if (!this._disposePromise) {
|
|
391
|
+
this._disposePromise = this._dispose();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await this._disposePromise;
|
|
328
395
|
}
|
|
329
396
|
|
|
330
397
|
/**
|
|
331
398
|
* Internal method to establish connection with or without push notification handling
|
|
332
399
|
*/
|
|
333
400
|
private async _connect(): Promise<void> {
|
|
334
|
-
|
|
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) {
|
|
335
416
|
throw new Error('No configuration provided');
|
|
336
417
|
}
|
|
337
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);
|
|
338
462
|
this._connectionState.next(TelnyxConnectionState.CONNECTING);
|
|
339
463
|
|
|
464
|
+
let client: TelnyxSDK.TelnyxRTC | undefined;
|
|
465
|
+
let canceledConnectCleanupCompleted = false;
|
|
466
|
+
|
|
340
467
|
try {
|
|
341
468
|
// Clean up existing client
|
|
342
469
|
if (this._telnyxClient) {
|
|
343
|
-
await this._telnyxClient
|
|
470
|
+
await this._disconnectClient(this._telnyxClient, 'Error during disconnect:');
|
|
344
471
|
}
|
|
345
472
|
|
|
473
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
474
|
+
|
|
346
475
|
// Create new client instance with authentication options
|
|
347
476
|
let clientOptions: TelnyxSDK.ClientOptions;
|
|
348
477
|
|
|
349
|
-
if (isCredentialConfig(
|
|
478
|
+
if (isCredentialConfig(config)) {
|
|
350
479
|
clientOptions = {
|
|
351
|
-
login:
|
|
352
|
-
password:
|
|
353
|
-
logLevel:
|
|
354
|
-
debug:
|
|
355
|
-
pushNotificationDeviceToken:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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,
|
|
360
492
|
sdkVersion: pkg.version,
|
|
361
493
|
};
|
|
362
494
|
console.log(
|
|
363
495
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
364
496
|
clientOptions.logLevel,
|
|
365
497
|
'pushToken:',
|
|
366
|
-
!!
|
|
498
|
+
!!config.pushNotificationDeviceToken
|
|
367
499
|
);
|
|
368
|
-
} else if (isTokenConfig(
|
|
500
|
+
} else if (isTokenConfig(config)) {
|
|
369
501
|
clientOptions = {
|
|
370
|
-
login_token:
|
|
371
|
-
logLevel:
|
|
372
|
-
debug:
|
|
373
|
-
pushNotificationDeviceToken:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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,
|
|
378
513
|
sdkVersion: pkg.version,
|
|
379
514
|
};
|
|
380
515
|
console.log(
|
|
381
516
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
382
517
|
clientOptions.logLevel,
|
|
383
518
|
'pushToken:',
|
|
384
|
-
!!
|
|
519
|
+
!!config.pushNotificationDeviceToken
|
|
385
520
|
);
|
|
386
521
|
} else {
|
|
387
522
|
throw new Error('Invalid configuration type');
|
|
388
523
|
}
|
|
389
524
|
|
|
390
|
-
|
|
525
|
+
client = new TelnyxSDK.TelnyxRTC(clientOptions);
|
|
526
|
+
this._telnyxClient = client;
|
|
391
527
|
|
|
392
528
|
// CRITICAL: Process any pending push notification payload BEFORE connecting
|
|
393
529
|
// This ensures voice_sdk_id and other payload variables are set before connect() is called
|
|
@@ -420,7 +556,7 @@ export class SessionManager {
|
|
|
420
556
|
(this as any)._pendingPushPayload = null;
|
|
421
557
|
}
|
|
422
558
|
|
|
423
|
-
this._setupClientListeners();
|
|
559
|
+
this._setupClientListeners(client);
|
|
424
560
|
|
|
425
561
|
// Set up CallStateController listeners immediately after client creation
|
|
426
562
|
// This ensures they're ready before any incoming call events are emitted
|
|
@@ -436,16 +572,38 @@ export class SessionManager {
|
|
|
436
572
|
console.log('🔧 SessionManager: No _onClientReady callback found');
|
|
437
573
|
}
|
|
438
574
|
|
|
575
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
576
|
+
|
|
439
577
|
// Connect to the platform AFTER processing push notification
|
|
440
578
|
console.log(
|
|
441
579
|
'SessionManager: RELEASE DEBUG - About to call connect() after processing push notification'
|
|
442
580
|
);
|
|
443
|
-
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
|
+
}
|
|
444
588
|
|
|
445
589
|
// Notify that client is ready for event listeners
|
|
446
590
|
console.log('🔧 SessionManager: Client connected successfully');
|
|
447
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
|
+
|
|
448
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
|
+
}
|
|
449
607
|
this._connectionState.next(TelnyxConnectionState.ERROR);
|
|
450
608
|
throw error;
|
|
451
609
|
}
|
|
@@ -454,12 +612,16 @@ export class SessionManager {
|
|
|
454
612
|
/**
|
|
455
613
|
* Set up event listeners for the Telnyx client
|
|
456
614
|
*/
|
|
457
|
-
private _setupClientListeners(): void {
|
|
458
|
-
if (!
|
|
615
|
+
private _setupClientListeners(client: TelnyxSDK.TelnyxRTC): void {
|
|
616
|
+
if (!client) {
|
|
459
617
|
return;
|
|
460
618
|
}
|
|
461
619
|
|
|
462
|
-
|
|
620
|
+
client.on('telnyx.client.ready', () => {
|
|
621
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
463
625
|
console.log('Telnyx client ready');
|
|
464
626
|
this._connectionState.next(TelnyxConnectionState.CONNECTED);
|
|
465
627
|
|
|
@@ -479,7 +641,11 @@ export class SessionManager {
|
|
|
479
641
|
}
|
|
480
642
|
});
|
|
481
643
|
|
|
482
|
-
|
|
644
|
+
client.on('telnyx.client.error', (error: Error) => {
|
|
645
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
483
649
|
console.error('Telnyx client error:', error);
|
|
484
650
|
this._connectionState.next(TelnyxConnectionState.ERROR);
|
|
485
651
|
});
|
|
@@ -544,4 +710,54 @@ export class SessionManager {
|
|
|
544
710
|
private _generateSessionId(): string {
|
|
545
711
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
546
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
|
+
}
|
|
547
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>;
|
|
@@ -38,6 +43,10 @@ export interface VoicePnBridgeInterface {
|
|
|
38
43
|
clearPendingVoipPush(): Promise<boolean>;
|
|
39
44
|
getPendingVoipAction(): Promise<string | null>;
|
|
40
45
|
clearPendingVoipAction(): Promise<boolean>;
|
|
46
|
+
setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
47
|
+
getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
48
|
+
setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
49
|
+
isSpeakerEnabled(): Promise<boolean>;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
const NativeBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
|
|
@@ -179,6 +188,48 @@ export class VoicePnBridge {
|
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
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
|
+
|
|
182
233
|
/**
|
|
183
234
|
* Get VoIP token from native storage
|
|
184
235
|
*/
|
|
@@ -244,6 +295,66 @@ export class VoicePnBridge {
|
|
|
244
295
|
}
|
|
245
296
|
}
|
|
246
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Enable or disable local missed call notifications on iOS.
|
|
300
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
301
|
+
*/
|
|
302
|
+
static async setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean> {
|
|
303
|
+
try {
|
|
304
|
+
return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('VoicePnBridge: Error setting missed call notifications:', error);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
313
|
+
*/
|
|
314
|
+
static async getMissedCallNotificationsEnabled(): Promise<boolean> {
|
|
315
|
+
try {
|
|
316
|
+
return await NativeBridge.getMissedCallNotificationsEnabled();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
325
|
+
* platform default voice route when disabled.
|
|
326
|
+
*/
|
|
327
|
+
static async setSpeakerEnabled(enabled: boolean): Promise<boolean> {
|
|
328
|
+
try {
|
|
329
|
+
return await NativeBridge.setSpeakerEnabled(enabled);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('VoicePnBridge: Error setting speaker route:', error);
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
338
|
+
*/
|
|
339
|
+
static async isSpeakerEnabled(): Promise<boolean> {
|
|
340
|
+
try {
|
|
341
|
+
return await NativeBridge.isSpeakerEnabled();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error('VoicePnBridge: Error getting speaker route:', error);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
350
|
+
*/
|
|
351
|
+
static async toggleSpeaker(): Promise<boolean> {
|
|
352
|
+
// This is intentionally read-then-write because route changes can also come from
|
|
353
|
+
// the OS, Bluetooth, wired accessories, or the active call session.
|
|
354
|
+
const enabled = await VoicePnBridge.isSpeakerEnabled();
|
|
355
|
+
return VoicePnBridge.setSpeakerEnabled(!enabled);
|
|
356
|
+
}
|
|
357
|
+
|
|
247
358
|
/**
|
|
248
359
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
249
360
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|