@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
|
@@ -59,6 +59,9 @@ const TelnyxSDK = __importStar(require('@telnyx/react-native-voice-sdk'));
|
|
|
59
59
|
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
|
+
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
63
|
+
const PUSH_WHEN_ACTIVE_STORAGE_KEY = '@push_when_active';
|
|
64
|
+
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
62
65
|
/**
|
|
63
66
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
64
67
|
*
|
|
@@ -71,6 +74,8 @@ class SessionManager {
|
|
|
71
74
|
connection_state_1.TelnyxConnectionState.DISCONNECTED
|
|
72
75
|
);
|
|
73
76
|
this._disposed = false;
|
|
77
|
+
this._disposing = false;
|
|
78
|
+
this._connectionGeneration = 0;
|
|
74
79
|
this._sessionId = this._generateSessionId();
|
|
75
80
|
}
|
|
76
81
|
/**
|
|
@@ -110,25 +115,26 @@ class SessionManager {
|
|
|
110
115
|
get telnyxClient() {
|
|
111
116
|
return this._telnyxClient;
|
|
112
117
|
}
|
|
118
|
+
get useTrickleIce() {
|
|
119
|
+
return Boolean(this._currentConfig?.useTrickleIce);
|
|
120
|
+
}
|
|
113
121
|
/**
|
|
114
122
|
* Connect using credential authentication
|
|
115
123
|
*/
|
|
116
124
|
async connectWithCredential(config) {
|
|
117
|
-
|
|
118
|
-
throw new Error('SessionManager has been disposed');
|
|
119
|
-
}
|
|
125
|
+
this._assertCanStartConnection();
|
|
120
126
|
this._currentConfig = config;
|
|
121
127
|
await this._connect();
|
|
128
|
+
this._assertCanStartConnection();
|
|
122
129
|
}
|
|
123
130
|
/**
|
|
124
131
|
* Connect using token authentication
|
|
125
132
|
*/
|
|
126
133
|
async connectWithToken(config) {
|
|
127
|
-
|
|
128
|
-
throw new Error('SessionManager has been disposed');
|
|
129
|
-
}
|
|
134
|
+
this._assertCanStartConnection();
|
|
130
135
|
this._currentConfig = config;
|
|
131
136
|
await this._connect();
|
|
137
|
+
this._assertCanStartConnection();
|
|
132
138
|
}
|
|
133
139
|
/**
|
|
134
140
|
* Disconnect from the Telnyx platform.
|
|
@@ -144,6 +150,7 @@ class SessionManager {
|
|
|
144
150
|
if (this._disposed) {
|
|
145
151
|
return;
|
|
146
152
|
}
|
|
153
|
+
this._connectionGeneration += 1;
|
|
147
154
|
this._currentConfig = undefined;
|
|
148
155
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
149
156
|
if (this._onDisconnect) {
|
|
@@ -154,11 +161,7 @@ class SessionManager {
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
if (this._telnyxClient) {
|
|
157
|
-
|
|
158
|
-
await this._telnyxClient.disconnect();
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error('Error during disconnect:', error);
|
|
161
|
-
}
|
|
164
|
+
await this._disconnectAndForgetClient(this._telnyxClient, 'Error during disconnect:');
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
/**
|
|
@@ -168,6 +171,7 @@ class SessionManager {
|
|
|
168
171
|
*/
|
|
169
172
|
disablePushNotifications() {
|
|
170
173
|
if (
|
|
174
|
+
!this._isTeardownActive() &&
|
|
171
175
|
this._telnyxClient &&
|
|
172
176
|
this.currentState === connection_state_1.TelnyxConnectionState.CONNECTED
|
|
173
177
|
) {
|
|
@@ -181,7 +185,7 @@ class SessionManager {
|
|
|
181
185
|
* Handle push notification with stored config
|
|
182
186
|
*/
|
|
183
187
|
handlePushNotificationWithConfig(pushMetaData, config) {
|
|
184
|
-
if (this.
|
|
188
|
+
if (this._isTeardownActive()) {
|
|
185
189
|
return;
|
|
186
190
|
}
|
|
187
191
|
this._currentConfig = config;
|
|
@@ -193,7 +197,7 @@ class SessionManager {
|
|
|
193
197
|
* Handle push notification (async version)
|
|
194
198
|
*/
|
|
195
199
|
async handlePushNotification(payload) {
|
|
196
|
-
if (this.
|
|
200
|
+
if (this._isTeardownActive()) {
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
203
|
console.log(
|
|
@@ -202,19 +206,33 @@ class SessionManager {
|
|
|
202
206
|
);
|
|
203
207
|
// Store the push notification payload for when the client is created
|
|
204
208
|
this._pendingPushPayload = payload;
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} catch (err) {
|
|
214
|
-
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');
|
|
215
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;
|
|
216
235
|
}
|
|
217
|
-
this._telnyxClient = undefined;
|
|
218
236
|
if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
|
|
219
237
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
220
238
|
}
|
|
@@ -231,12 +249,26 @@ class SessionManager {
|
|
|
231
249
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
232
250
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
233
251
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
252
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
253
|
+
const storedPushWhenActive = await AsyncStorage.getItem(PUSH_WHEN_ACTIVE_STORAGE_KEY);
|
|
254
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
255
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
256
|
+
);
|
|
257
|
+
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
258
|
+
const pushWhenActive = storedPushWhenActive === 'true';
|
|
259
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
260
|
+
if (this._isTeardownActive()) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
234
263
|
// Check if we have credential-based authentication data
|
|
235
264
|
if (storedUsername && storedPassword) {
|
|
236
265
|
console.log('SessionManager: RELEASE DEBUG - Found stored credentials, creating config');
|
|
237
266
|
const { createCredentialConfig } = require('../../models/config');
|
|
238
267
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
239
268
|
pushNotificationDeviceToken: storedPushToken,
|
|
269
|
+
useTrickleIce,
|
|
270
|
+
pushWhenActive,
|
|
271
|
+
enableMissedCallNotifications,
|
|
240
272
|
});
|
|
241
273
|
}
|
|
242
274
|
// Check if we have token-based authentication data
|
|
@@ -245,6 +277,9 @@ class SessionManager {
|
|
|
245
277
|
const { createTokenConfig } = require('../../models/config');
|
|
246
278
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
247
279
|
pushNotificationDeviceToken: storedPushToken,
|
|
280
|
+
useTrickleIce,
|
|
281
|
+
pushWhenActive,
|
|
282
|
+
enableMissedCallNotifications,
|
|
248
283
|
});
|
|
249
284
|
}
|
|
250
285
|
if (this._currentConfig) {
|
|
@@ -258,6 +293,9 @@ class SessionManager {
|
|
|
258
293
|
console.warn('SessionManager: Failed to load stored config for push notification:', error);
|
|
259
294
|
}
|
|
260
295
|
}
|
|
296
|
+
if (this._isTeardownActive()) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
261
299
|
// If we already have a client, process the push notification immediately
|
|
262
300
|
if (this._telnyxClient) {
|
|
263
301
|
console.log(
|
|
@@ -302,10 +340,16 @@ class SessionManager {
|
|
|
302
340
|
);
|
|
303
341
|
try {
|
|
304
342
|
await this._connect();
|
|
343
|
+
if (this._isTeardownActive()) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
305
346
|
console.log(
|
|
306
347
|
'SessionManager: RELEASE DEBUG - Successfully connected after push notification trigger'
|
|
307
348
|
);
|
|
308
349
|
} catch (error) {
|
|
350
|
+
if (this._isTeardownActive()) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
309
353
|
console.error(
|
|
310
354
|
'SessionManager: Failed to connect after push notification trigger:',
|
|
311
355
|
error
|
|
@@ -325,73 +369,138 @@ class SessionManager {
|
|
|
325
369
|
}
|
|
326
370
|
console.log('SessionManager: RELEASE DEBUG - Push notification handling complete');
|
|
327
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
|
+
}
|
|
328
379
|
/**
|
|
329
380
|
* Dispose of the session manager and clean up resources
|
|
330
381
|
*/
|
|
331
|
-
dispose() {
|
|
382
|
+
async dispose() {
|
|
332
383
|
if (this._disposed) {
|
|
333
384
|
return;
|
|
334
385
|
}
|
|
335
|
-
this.
|
|
336
|
-
|
|
337
|
-
|
|
386
|
+
if (!this._disposePromise) {
|
|
387
|
+
this._disposePromise = this._dispose();
|
|
388
|
+
}
|
|
389
|
+
await this._disposePromise;
|
|
338
390
|
}
|
|
339
391
|
/**
|
|
340
392
|
* Internal method to establish connection with or without push notification handling
|
|
341
393
|
*/
|
|
342
394
|
async _connect() {
|
|
343
|
-
|
|
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) {
|
|
344
408
|
throw new Error('No configuration provided');
|
|
345
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);
|
|
346
445
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTING);
|
|
446
|
+
let client;
|
|
447
|
+
let canceledConnectCleanupCompleted = false;
|
|
347
448
|
try {
|
|
348
449
|
// Clean up existing client
|
|
349
450
|
if (this._telnyxClient) {
|
|
350
|
-
await this._telnyxClient
|
|
451
|
+
await this._disconnectClient(this._telnyxClient, 'Error during disconnect:');
|
|
351
452
|
}
|
|
453
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
352
454
|
// Create new client instance with authentication options
|
|
353
455
|
let clientOptions;
|
|
354
|
-
if ((0, config_1.isCredentialConfig)(
|
|
456
|
+
if ((0, config_1.isCredentialConfig)(config)) {
|
|
355
457
|
clientOptions = {
|
|
356
|
-
login:
|
|
357
|
-
password:
|
|
358
|
-
logLevel:
|
|
359
|
-
debug:
|
|
360
|
-
pushNotificationDeviceToken:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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,
|
|
365
470
|
sdkVersion: pkg.version,
|
|
366
471
|
};
|
|
367
472
|
console.log(
|
|
368
473
|
'🔧 SessionManager: Creating TelnyxRTC with credential config, logLevel:',
|
|
369
474
|
clientOptions.logLevel,
|
|
370
475
|
'pushToken:',
|
|
371
|
-
!!
|
|
476
|
+
!!config.pushNotificationDeviceToken
|
|
372
477
|
);
|
|
373
|
-
} else if ((0, config_1.isTokenConfig)(
|
|
478
|
+
} else if ((0, config_1.isTokenConfig)(config)) {
|
|
374
479
|
clientOptions = {
|
|
375
|
-
login_token:
|
|
376
|
-
logLevel:
|
|
377
|
-
debug:
|
|
378
|
-
pushNotificationDeviceToken:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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,
|
|
383
491
|
sdkVersion: pkg.version,
|
|
384
492
|
};
|
|
385
493
|
console.log(
|
|
386
494
|
'🔧 SessionManager: Creating TelnyxRTC with token config, logLevel:',
|
|
387
495
|
clientOptions.logLevel,
|
|
388
496
|
'pushToken:',
|
|
389
|
-
!!
|
|
497
|
+
!!config.pushNotificationDeviceToken
|
|
390
498
|
);
|
|
391
499
|
} else {
|
|
392
500
|
throw new Error('Invalid configuration type');
|
|
393
501
|
}
|
|
394
|
-
|
|
502
|
+
client = new TelnyxSDK.TelnyxRTC(clientOptions);
|
|
503
|
+
this._telnyxClient = client;
|
|
395
504
|
// CRITICAL: Process any pending push notification payload BEFORE connecting
|
|
396
505
|
// This ensures voice_sdk_id and other payload variables are set before connect() is called
|
|
397
506
|
const pendingPushPayload = this._pendingPushPayload;
|
|
@@ -418,7 +527,7 @@ class SessionManager {
|
|
|
418
527
|
// Clear the pending payload
|
|
419
528
|
this._pendingPushPayload = null;
|
|
420
529
|
}
|
|
421
|
-
this._setupClientListeners();
|
|
530
|
+
this._setupClientListeners(client);
|
|
422
531
|
// Set up CallStateController listeners immediately after client creation
|
|
423
532
|
// This ensures they're ready before any incoming call events are emitted
|
|
424
533
|
console.log(
|
|
@@ -432,15 +541,34 @@ class SessionManager {
|
|
|
432
541
|
} else {
|
|
433
542
|
console.log('🔧 SessionManager: No _onClientReady callback found');
|
|
434
543
|
}
|
|
544
|
+
this._throwIfConnectCanceled(connectionGeneration);
|
|
435
545
|
// Connect to the platform AFTER processing push notification
|
|
436
546
|
console.log(
|
|
437
547
|
'SessionManager: RELEASE DEBUG - About to call connect() after processing push notification'
|
|
438
548
|
);
|
|
439
|
-
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
|
+
}
|
|
440
555
|
// Notify that client is ready for event listeners
|
|
441
556
|
console.log('🔧 SessionManager: Client connected successfully');
|
|
442
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
|
+
}
|
|
443
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
|
+
}
|
|
444
572
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
|
|
445
573
|
throw error;
|
|
446
574
|
}
|
|
@@ -448,11 +576,14 @@ class SessionManager {
|
|
|
448
576
|
/**
|
|
449
577
|
* Set up event listeners for the Telnyx client
|
|
450
578
|
*/
|
|
451
|
-
_setupClientListeners() {
|
|
452
|
-
if (!
|
|
579
|
+
_setupClientListeners(client) {
|
|
580
|
+
if (!client) {
|
|
453
581
|
return;
|
|
454
582
|
}
|
|
455
|
-
|
|
583
|
+
client.on('telnyx.client.ready', () => {
|
|
584
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
456
587
|
console.log('Telnyx client ready');
|
|
457
588
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.CONNECTED);
|
|
458
589
|
// Ensure CallStateController listeners are set up when client becomes ready
|
|
@@ -470,7 +601,10 @@ class SessionManager {
|
|
|
470
601
|
console.log('🔧 SessionManager: No _onClientReady callback found in client ready event');
|
|
471
602
|
}
|
|
472
603
|
});
|
|
473
|
-
|
|
604
|
+
client.on('telnyx.client.error', (error) => {
|
|
605
|
+
if (this._isTeardownActive() || this._telnyxClient !== client) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
474
608
|
console.error('Telnyx client error:', error);
|
|
475
609
|
this._connectionState.next(connection_state_1.TelnyxConnectionState.ERROR);
|
|
476
610
|
});
|
|
@@ -526,5 +660,42 @@ class SessionManager {
|
|
|
526
660
|
_generateSessionId() {
|
|
527
661
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
528
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
|
+
}
|
|
529
700
|
}
|
|
530
701
|
exports.SessionManager = SessionManager;
|
|
@@ -25,11 +25,18 @@ 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>;
|
|
31
34
|
getPendingVoipAction(): Promise<string | null>;
|
|
32
35
|
clearPendingVoipAction(): Promise<boolean>;
|
|
36
|
+
setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
37
|
+
getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
38
|
+
setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
39
|
+
isSpeakerEnabled(): Promise<boolean>;
|
|
33
40
|
}
|
|
34
41
|
declare const NativeBridge: VoicePnBridgeInterface;
|
|
35
42
|
/**
|
|
@@ -87,6 +94,21 @@ export declare class VoicePnBridge {
|
|
|
87
94
|
* Useful for dismissing notifications when call is answered/rejected in app
|
|
88
95
|
*/
|
|
89
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>;
|
|
90
112
|
/**
|
|
91
113
|
* Get VoIP token from native storage
|
|
92
114
|
*/
|
|
@@ -107,6 +129,28 @@ export declare class VoicePnBridge {
|
|
|
107
129
|
* Clear pending VoIP action from native storage (iOS only)
|
|
108
130
|
*/
|
|
109
131
|
static clearPendingVoipAction(): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Enable or disable local missed call notifications on iOS.
|
|
134
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
135
|
+
*/
|
|
136
|
+
static setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
139
|
+
*/
|
|
140
|
+
static getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
141
|
+
/**
|
|
142
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
143
|
+
* platform default voice route when disabled.
|
|
144
|
+
*/
|
|
145
|
+
static setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
148
|
+
*/
|
|
149
|
+
static isSpeakerEnabled(): Promise<boolean>;
|
|
150
|
+
/**
|
|
151
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
152
|
+
*/
|
|
153
|
+
static toggleSpeaker(): Promise<boolean>;
|
|
110
154
|
/**
|
|
111
155
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
112
156
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|
|
@@ -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
|
*/
|
|
@@ -184,6 +223,61 @@ class VoicePnBridge {
|
|
|
184
223
|
return false;
|
|
185
224
|
}
|
|
186
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Enable or disable local missed call notifications on iOS.
|
|
228
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
229
|
+
*/
|
|
230
|
+
static async setMissedCallNotificationsEnabled(enabled) {
|
|
231
|
+
try {
|
|
232
|
+
return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('VoicePnBridge: Error setting missed call notifications:', error);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
240
|
+
*/
|
|
241
|
+
static async getMissedCallNotificationsEnabled() {
|
|
242
|
+
try {
|
|
243
|
+
return await NativeBridge.getMissedCallNotificationsEnabled();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
251
|
+
* platform default voice route when disabled.
|
|
252
|
+
*/
|
|
253
|
+
static async setSpeakerEnabled(enabled) {
|
|
254
|
+
try {
|
|
255
|
+
return await NativeBridge.setSpeakerEnabled(enabled);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('VoicePnBridge: Error setting speaker route:', error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
263
|
+
*/
|
|
264
|
+
static async isSpeakerEnabled() {
|
|
265
|
+
try {
|
|
266
|
+
return await NativeBridge.isSpeakerEnabled();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('VoicePnBridge: Error getting speaker route:', error);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
274
|
+
*/
|
|
275
|
+
static async toggleSpeaker() {
|
|
276
|
+
// This is intentionally read-then-write because route changes can also come from
|
|
277
|
+
// the OS, Bluetooth, wired accessories, or the active call session.
|
|
278
|
+
const enabled = await VoicePnBridge.isSpeakerEnabled();
|
|
279
|
+
return VoicePnBridge.setSpeakerEnabled(!enabled);
|
|
280
|
+
}
|
|
187
281
|
/**
|
|
188
282
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
189
283
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|