@telnyx/react-voice-commons-sdk 0.4.2 → 1.0.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 +11 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +55 -1
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +17 -1
- package/ios/CallKitBridge.swift +426 -0
- package/ios/VoicePnBridge.m +15 -1
- package/ios/VoicePnBridge.swift +47 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/lib/internal/calls/call-state-controller.d.ts +6 -0
- package/lib/internal/calls/call-state-controller.js +52 -36
- package/lib/internal/session/session-manager.d.ts +1 -0
- package/lib/internal/session/session-manager.js +32 -30
- package/lib/internal/voice-pn-bridge.d.ts +26 -0
- package/lib/internal/voice-pn-bridge.js +55 -0
- package/lib/models/config.d.ts +8 -0
- package/lib/telnyx-voip-client.d.ts +2 -0
- package/lib/telnyx-voip-client.js +49 -18
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/internal/calls/call-state-controller.ts +61 -45
- package/src/internal/session/session-manager.ts +34 -30
- package/src/internal/voice-pn-bridge.ts +64 -0
- package/src/models/config.ts +8 -0
- package/src/telnyx-voip-client.ts +55 -19
- package/src/types/telnyx-sdk.d.ts +10 -0
|
@@ -17,6 +17,7 @@ export class CallStateController {
|
|
|
17
17
|
private readonly _calls = new BehaviorSubject<Call[]>([]);
|
|
18
18
|
private readonly _callMap = new Map<string, Call>();
|
|
19
19
|
private _disposed = false;
|
|
20
|
+
private _listenerClient?: TelnyxRTC;
|
|
20
21
|
|
|
21
22
|
// Callbacks for waiting for invite logic (used for push notifications)
|
|
22
23
|
private _isWaitingForInvite?: () => boolean;
|
|
@@ -155,6 +156,9 @@ export class CallStateController {
|
|
|
155
156
|
callerIdName: callerName,
|
|
156
157
|
callerIdNumber: callerNumber,
|
|
157
158
|
customHeaders,
|
|
159
|
+
peerConnectionOptions: {
|
|
160
|
+
useTrickleIce: this._sessionManager.useTrickleIce,
|
|
161
|
+
},
|
|
158
162
|
};
|
|
159
163
|
const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
|
|
160
164
|
|
|
@@ -225,6 +229,7 @@ export class CallStateController {
|
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
this._disposed = true;
|
|
232
|
+
this._removeClientListeners();
|
|
228
233
|
|
|
229
234
|
// Dispose of all calls
|
|
230
235
|
this.currentCalls.forEach((call) => call.dispose());
|
|
@@ -241,40 +246,28 @@ export class CallStateController {
|
|
|
241
246
|
private _setupClientListeners(): void {
|
|
242
247
|
console.log('CallStateController: Setting up client listeners...');
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
250
|
+
|
|
251
|
+
if (!telnyxClient) {
|
|
245
252
|
console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
|
|
246
253
|
return;
|
|
247
254
|
}
|
|
248
255
|
|
|
256
|
+
this._removeClientListeners();
|
|
257
|
+
this._listenerClient = telnyxClient;
|
|
258
|
+
|
|
249
259
|
console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
|
|
250
|
-
console.log(
|
|
251
|
-
'CallStateController: Client instance:',
|
|
252
|
-
this._sessionManager.telnyxClient.constructor.name
|
|
253
|
-
);
|
|
260
|
+
console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
|
|
254
261
|
|
|
255
262
|
// Listen for incoming calls
|
|
256
|
-
|
|
257
|
-
'telnyx.call.incoming',
|
|
258
|
-
(telnyxCall: TelnyxCall, msg: any) => {
|
|
259
|
-
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
260
|
-
this._handleIncomingCall(telnyxCall, msg, false);
|
|
261
|
-
}
|
|
262
|
-
);
|
|
263
|
+
telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
263
264
|
|
|
264
265
|
// Listen for reattached calls (after network reconnection)
|
|
265
|
-
|
|
266
|
-
'telnyx.call.reattached',
|
|
267
|
-
(telnyxCall: TelnyxCall, msg: any) => {
|
|
268
|
-
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
269
|
-
this._handleIncomingCall(telnyxCall, msg, true);
|
|
270
|
-
}
|
|
271
|
-
);
|
|
266
|
+
telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
272
267
|
|
|
273
268
|
// Verify listeners are set up
|
|
274
|
-
const incomingListeners =
|
|
275
|
-
|
|
276
|
-
const reattachedListeners =
|
|
277
|
-
this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
|
|
269
|
+
const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
|
|
270
|
+
const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
|
|
278
271
|
console.log(
|
|
279
272
|
'CallStateController: Listeners registered - incoming:',
|
|
280
273
|
incomingListeners,
|
|
@@ -283,34 +276,57 @@ export class CallStateController {
|
|
|
283
276
|
);
|
|
284
277
|
|
|
285
278
|
// Listen for call state changes from the TelnyxRTC client (multi-call support)
|
|
286
|
-
|
|
287
|
-
'telnyx.call.stateChanged',
|
|
288
|
-
(telnyxCall: TelnyxCall, state: string) => {
|
|
289
|
-
console.log(
|
|
290
|
-
'CallStateController: Call state changed from TelnyxRTC:',
|
|
291
|
-
telnyxCall.callId,
|
|
292
|
-
state
|
|
293
|
-
);
|
|
294
|
-
// Find our wrapper call and update if needed
|
|
295
|
-
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
296
|
-
if (call) {
|
|
297
|
-
console.log(
|
|
298
|
-
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
);
|
|
279
|
+
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
303
280
|
|
|
304
281
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
305
|
-
|
|
306
|
-
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
307
|
-
// The call cleanup is already handled by our call state subscription
|
|
308
|
-
// This event is informational for logging/debugging
|
|
309
|
-
});
|
|
282
|
+
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
310
283
|
|
|
311
284
|
console.log('CallStateController: Client listeners set up successfully');
|
|
312
285
|
}
|
|
313
286
|
|
|
287
|
+
private _removeClientListeners(): void {
|
|
288
|
+
if (!this._listenerClient) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
293
|
+
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
294
|
+
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
295
|
+
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
296
|
+
this._listenerClient = undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private _handleTelnyxIncomingCall = (telnyxCall: TelnyxCall, msg: any): void => {
|
|
300
|
+
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
301
|
+
this._handleIncomingCall(telnyxCall, msg, false);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
private _handleTelnyxReattachedCall = (telnyxCall: TelnyxCall, msg: any): void => {
|
|
305
|
+
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
306
|
+
this._handleIncomingCall(telnyxCall, msg, true);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
private _handleTelnyxCallStateChanged = (telnyxCall: TelnyxCall, state: string): void => {
|
|
310
|
+
console.log(
|
|
311
|
+
'CallStateController: Call state changed from TelnyxRTC:',
|
|
312
|
+
telnyxCall.callId,
|
|
313
|
+
state
|
|
314
|
+
);
|
|
315
|
+
// Find our wrapper call and update if needed
|
|
316
|
+
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
317
|
+
if (call) {
|
|
318
|
+
console.log(
|
|
319
|
+
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
private _handleTelnyxCallRemoved = (callId: string): void => {
|
|
325
|
+
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
326
|
+
// The call cleanup is already handled by our call state subscription.
|
|
327
|
+
// This event is informational for logging/debugging.
|
|
328
|
+
};
|
|
329
|
+
|
|
314
330
|
/**
|
|
315
331
|
* Handle incoming call or reattached call
|
|
316
332
|
*/
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
isTokenConfig,
|
|
12
12
|
} from '../../models/config';
|
|
13
13
|
|
|
14
|
+
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
15
|
+
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
16
19
|
*
|
|
@@ -75,6 +78,10 @@ export class SessionManager {
|
|
|
75
78
|
return this._telnyxClient;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
get useTrickleIce(): boolean {
|
|
82
|
+
return Boolean(this._currentConfig?.useTrickleIce);
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
/**
|
|
79
86
|
* Connect using credential authentication
|
|
80
87
|
*/
|
|
@@ -178,39 +185,22 @@ export class SessionManager {
|
|
|
178
185
|
// Store the push notification payload for when the client is created
|
|
179
186
|
(this as any)._pendingPushPayload = payload;
|
|
180
187
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// This handles two cases iOS gives us with the same code path:
|
|
187
|
-
// 1. Terminated-then-cold-launched: a client exists (constructed at
|
|
188
|
-
// app boot) but its connection was never opened (idleMs=Infinity).
|
|
189
|
-
// 2. Suspended-then-thawed: a client exists with `connected=true`,
|
|
190
|
-
// but the kernel killed the TLS session during freeze without
|
|
191
|
-
// notifying the JS WebSocket wrapper, so no error event fired.
|
|
192
|
-
//
|
|
193
|
-
// 30s threshold matches the server's keep-alive ping cadence (~20s),
|
|
194
|
-
// so a live session always remains fresh.
|
|
195
|
-
const STALE_THRESHOLD_MS = 30000;
|
|
188
|
+
// Each push always rebuilds the TelnyxRTC client. The voice_sdk_id is
|
|
189
|
+
// baked into the WebSocket URL at socket-open time and can't be mutated
|
|
190
|
+
// mid-flight; reusing a connection from a previous push means the
|
|
191
|
+
// gateway routes THIS push's INVITE to a different (correctly-stamped)
|
|
192
|
+
// client and we sit on the wrong socket forever.
|
|
196
193
|
if (this._telnyxClient) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
: !!client.connected;
|
|
202
|
-
if (!isFresh) {
|
|
203
|
-
try {
|
|
204
|
-
await this._telnyxClient.disconnect();
|
|
205
|
-
} catch (err) {
|
|
206
|
-
console.warn('SessionManager: disconnect of stale client threw:', err);
|
|
207
|
-
}
|
|
208
|
-
this._telnyxClient = undefined;
|
|
209
|
-
if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
|
|
210
|
-
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
211
|
-
}
|
|
194
|
+
try {
|
|
195
|
+
await this._telnyxClient.disconnect();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.warn('SessionManager: disconnect of prior client threw:', err);
|
|
212
198
|
}
|
|
213
199
|
}
|
|
200
|
+
this._telnyxClient = undefined;
|
|
201
|
+
if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
|
|
202
|
+
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
203
|
+
}
|
|
214
204
|
|
|
215
205
|
// If we don't have a config yet but we're processing a push notification,
|
|
216
206
|
// attempt to load stored config first (for terminated app startup)
|
|
@@ -227,6 +217,12 @@ export class SessionManager {
|
|
|
227
217
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
228
218
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
229
219
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
220
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
221
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
222
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
223
|
+
);
|
|
224
|
+
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
225
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
230
226
|
|
|
231
227
|
// Check if we have credential-based authentication data
|
|
232
228
|
if (storedUsername && storedPassword) {
|
|
@@ -234,6 +230,8 @@ export class SessionManager {
|
|
|
234
230
|
const { createCredentialConfig } = require('../../models/config');
|
|
235
231
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
236
232
|
pushNotificationDeviceToken: storedPushToken,
|
|
233
|
+
useTrickleIce,
|
|
234
|
+
enableMissedCallNotifications,
|
|
237
235
|
});
|
|
238
236
|
}
|
|
239
237
|
// Check if we have token-based authentication data
|
|
@@ -242,6 +240,8 @@ export class SessionManager {
|
|
|
242
240
|
const { createTokenConfig } = require('../../models/config');
|
|
243
241
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
244
242
|
pushNotificationDeviceToken: storedPushToken,
|
|
243
|
+
useTrickleIce,
|
|
244
|
+
enableMissedCallNotifications,
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -370,6 +370,8 @@ export class SessionManager {
|
|
|
370
370
|
logLevel: this._currentConfig.debug ? 'debug' : 'warn',
|
|
371
371
|
debug: this._currentConfig.debug ?? false,
|
|
372
372
|
pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
|
|
373
|
+
enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
|
|
374
|
+
useTrickleIce: this._currentConfig.useTrickleIce,
|
|
373
375
|
enableCallReports: this._currentConfig.enableCallReports,
|
|
374
376
|
callReportInterval: this._currentConfig.callReportInterval,
|
|
375
377
|
callReportLogLevel: this._currentConfig.callReportLogLevel,
|
|
@@ -388,6 +390,8 @@ export class SessionManager {
|
|
|
388
390
|
logLevel: this._currentConfig.debug ? 'debug' : 'warn',
|
|
389
391
|
debug: this._currentConfig.debug ?? false,
|
|
390
392
|
pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
|
|
393
|
+
enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
|
|
394
|
+
useTrickleIce: this._currentConfig.useTrickleIce,
|
|
391
395
|
enableCallReports: this._currentConfig.enableCallReports,
|
|
392
396
|
callReportInterval: this._currentConfig.callReportInterval,
|
|
393
397
|
callReportLogLevel: this._currentConfig.callReportLogLevel,
|
|
@@ -38,6 +38,10 @@ export interface VoicePnBridgeInterface {
|
|
|
38
38
|
clearPendingVoipPush(): Promise<boolean>;
|
|
39
39
|
getPendingVoipAction(): Promise<string | null>;
|
|
40
40
|
clearPendingVoipAction(): Promise<boolean>;
|
|
41
|
+
setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
42
|
+
getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
43
|
+
setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
44
|
+
isSpeakerEnabled(): Promise<boolean>;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
const NativeBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
|
|
@@ -244,6 +248,66 @@ export class VoicePnBridge {
|
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Enable or disable local missed call notifications on iOS.
|
|
253
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
254
|
+
*/
|
|
255
|
+
static async setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean> {
|
|
256
|
+
try {
|
|
257
|
+
return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('VoicePnBridge: Error setting missed call notifications:', error);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
266
|
+
*/
|
|
267
|
+
static async getMissedCallNotificationsEnabled(): Promise<boolean> {
|
|
268
|
+
try {
|
|
269
|
+
return await NativeBridge.getMissedCallNotificationsEnabled();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
278
|
+
* platform default voice route when disabled.
|
|
279
|
+
*/
|
|
280
|
+
static async setSpeakerEnabled(enabled: boolean): Promise<boolean> {
|
|
281
|
+
try {
|
|
282
|
+
return await NativeBridge.setSpeakerEnabled(enabled);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('VoicePnBridge: Error setting speaker route:', error);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
291
|
+
*/
|
|
292
|
+
static async isSpeakerEnabled(): Promise<boolean> {
|
|
293
|
+
try {
|
|
294
|
+
return await NativeBridge.isSpeakerEnabled();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('VoicePnBridge: Error getting speaker route:', error);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
303
|
+
*/
|
|
304
|
+
static async toggleSpeaker(): Promise<boolean> {
|
|
305
|
+
// This is intentionally read-then-write because route changes can also come from
|
|
306
|
+
// the OS, Bluetooth, wired accessories, or the active call session.
|
|
307
|
+
const enabled = await VoicePnBridge.isSpeakerEnabled();
|
|
308
|
+
return VoicePnBridge.setSpeakerEnabled(!enabled);
|
|
309
|
+
}
|
|
310
|
+
|
|
247
311
|
/**
|
|
248
312
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
249
313
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|
package/src/models/config.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface CredentialConfig {
|
|
|
7
7
|
sipPassword: string;
|
|
8
8
|
debug?: boolean;
|
|
9
9
|
pushNotificationDeviceToken?: string;
|
|
10
|
+
/** Enable native missed call push notifications. Default: false */
|
|
11
|
+
enableMissedCallNotifications?: boolean;
|
|
12
|
+
/** Enable Trickle ICE. Default: false */
|
|
13
|
+
useTrickleIce?: boolean;
|
|
10
14
|
/** Enable automatic call quality reporting. Default: true */
|
|
11
15
|
enableCallReports?: boolean;
|
|
12
16
|
/** Stats collection interval in seconds. Default: 5 */
|
|
@@ -25,6 +29,10 @@ export interface TokenConfig {
|
|
|
25
29
|
token: string;
|
|
26
30
|
debug?: boolean;
|
|
27
31
|
pushNotificationDeviceToken?: string;
|
|
32
|
+
/** Enable native missed call push notifications. Default: false */
|
|
33
|
+
enableMissedCallNotifications?: boolean;
|
|
34
|
+
/** Enable Trickle ICE. Default: false */
|
|
35
|
+
useTrickleIce?: boolean;
|
|
28
36
|
/** Enable automatic call quality reporting. Default: true */
|
|
29
37
|
enableCallReports?: boolean;
|
|
30
38
|
/** Stats collection interval in seconds. Default: 5 */
|
|
@@ -7,6 +7,9 @@ import { SessionManager } from './internal/session/session-manager';
|
|
|
7
7
|
import { CallStateController } from './internal/calls/call-state-controller';
|
|
8
8
|
import { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
9
9
|
|
|
10
|
+
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
11
|
+
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Configuration options for TelnyxVoipClient
|
|
12
15
|
*/
|
|
@@ -16,6 +19,9 @@ export interface TelnyxVoipClientOptions {
|
|
|
16
19
|
|
|
17
20
|
/** Enable debug logging */
|
|
18
21
|
debug?: boolean;
|
|
22
|
+
|
|
23
|
+
/** Enable Trickle ICE for calls created by this client */
|
|
24
|
+
useTrickleIce?: boolean;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
@@ -67,6 +73,7 @@ export class TelnyxVoipClient {
|
|
|
67
73
|
this._options = {
|
|
68
74
|
enableAppStateManagement: true,
|
|
69
75
|
debug: false,
|
|
76
|
+
useTrickleIce: false,
|
|
70
77
|
...options,
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -218,10 +225,15 @@ export class TelnyxVoipClient {
|
|
|
218
225
|
console.log('TelnyxVoipClient: Logging in with credentials for user:', config.sipUser);
|
|
219
226
|
}
|
|
220
227
|
|
|
228
|
+
const loginConfig = {
|
|
229
|
+
...config,
|
|
230
|
+
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
231
|
+
};
|
|
232
|
+
|
|
221
233
|
// Store credentials for future reconnection
|
|
222
|
-
await this._storeCredentials(
|
|
234
|
+
await this._storeCredentials(loginConfig);
|
|
223
235
|
|
|
224
|
-
await this._sessionManager.connectWithCredential(
|
|
236
|
+
await this._sessionManager.connectWithCredential(loginConfig);
|
|
225
237
|
}
|
|
226
238
|
|
|
227
239
|
/**
|
|
@@ -245,10 +257,15 @@ export class TelnyxVoipClient {
|
|
|
245
257
|
console.log('TelnyxVoipClient: Logging in with token');
|
|
246
258
|
}
|
|
247
259
|
|
|
260
|
+
const loginConfig = {
|
|
261
|
+
...config,
|
|
262
|
+
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
263
|
+
};
|
|
264
|
+
|
|
248
265
|
// Store token for future reconnection
|
|
249
|
-
await this._storeToken(
|
|
266
|
+
await this._storeToken(loginConfig);
|
|
250
267
|
|
|
251
|
-
await this._sessionManager.connectWithToken(
|
|
268
|
+
await this._sessionManager.connectWithToken(loginConfig);
|
|
252
269
|
}
|
|
253
270
|
|
|
254
271
|
/**
|
|
@@ -292,6 +309,13 @@ export class TelnyxVoipClient {
|
|
|
292
309
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
293
310
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
294
311
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
312
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
313
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
314
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
315
|
+
);
|
|
316
|
+
const useTrickleIce =
|
|
317
|
+
storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
|
|
318
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
295
319
|
|
|
296
320
|
// Check if we have credential-based authentication data
|
|
297
321
|
if (storedUsername && storedPassword) {
|
|
@@ -299,6 +323,8 @@ export class TelnyxVoipClient {
|
|
|
299
323
|
const { createCredentialConfig } = require('./models/config');
|
|
300
324
|
const config = createCredentialConfig(storedUsername, storedPassword, {
|
|
301
325
|
pushNotificationDeviceToken: storedPushToken,
|
|
326
|
+
useTrickleIce,
|
|
327
|
+
enableMissedCallNotifications,
|
|
302
328
|
});
|
|
303
329
|
|
|
304
330
|
if (this._options.debug) {
|
|
@@ -318,6 +344,8 @@ export class TelnyxVoipClient {
|
|
|
318
344
|
const { createTokenConfig } = require('./models/config');
|
|
319
345
|
const config = createTokenConfig(storedCredentialToken, {
|
|
320
346
|
pushNotificationDeviceToken: storedPushToken,
|
|
347
|
+
useTrickleIce,
|
|
348
|
+
enableMissedCallNotifications,
|
|
321
349
|
});
|
|
322
350
|
|
|
323
351
|
if (this._options.debug) {
|
|
@@ -401,22 +429,14 @@ export class TelnyxVoipClient {
|
|
|
401
429
|
}
|
|
402
430
|
|
|
403
431
|
try {
|
|
404
|
-
//
|
|
405
|
-
//
|
|
432
|
+
// SessionManager owns the entire push lifecycle: it disposes any prior
|
|
433
|
+
// client, loads stored credentials if needed, and rebuilds the
|
|
434
|
+
// TelnyxRTC bound to THIS push's voice_sdk_id. We deliberately do not
|
|
435
|
+
// fall back to a generic loginFromStoredConfig() here — that would
|
|
436
|
+
// create a parallel client with no voice_sdk_id awareness, which the
|
|
437
|
+
// gateway then has to `punt` once the SessionManager-driven session
|
|
438
|
+
// also registers, dropping the active call.
|
|
406
439
|
await this._sessionManager.handlePushNotification(payload);
|
|
407
|
-
|
|
408
|
-
// Connect if not already connected
|
|
409
|
-
const currentState = this.currentConnectionState;
|
|
410
|
-
if (currentState !== TelnyxConnectionState.CONNECTED) {
|
|
411
|
-
// Try to login from stored config - now the push flags should be set
|
|
412
|
-
const loginSuccess = await this.loginFromStoredConfig();
|
|
413
|
-
if (!loginSuccess) {
|
|
414
|
-
console.warn(
|
|
415
|
-
'TelnyxVoipClient: Could not login from stored config for push notification'
|
|
416
|
-
);
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
440
|
} catch (error) {
|
|
421
441
|
console.error('TelnyxVoipClient: Error handling push notification:', error);
|
|
422
442
|
throw error;
|
|
@@ -536,6 +556,14 @@ export class TelnyxVoipClient {
|
|
|
536
556
|
|
|
537
557
|
await AsyncStorage.setItem('@telnyx_username', config.sipUser);
|
|
538
558
|
await AsyncStorage.setItem('@telnyx_password', config.sipPassword);
|
|
559
|
+
await AsyncStorage.setItem(
|
|
560
|
+
USE_TRICKLE_ICE_STORAGE_KEY,
|
|
561
|
+
String(config.useTrickleIce ?? false)
|
|
562
|
+
);
|
|
563
|
+
await AsyncStorage.setItem(
|
|
564
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
565
|
+
String(config.enableMissedCallNotifications ?? false)
|
|
566
|
+
);
|
|
539
567
|
|
|
540
568
|
if (config.pushNotificationDeviceToken) {
|
|
541
569
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
@@ -563,6 +591,14 @@ export class TelnyxVoipClient {
|
|
|
563
591
|
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
564
592
|
|
|
565
593
|
await AsyncStorage.setItem('@credential_token', config.token);
|
|
594
|
+
await AsyncStorage.setItem(
|
|
595
|
+
USE_TRICKLE_ICE_STORAGE_KEY,
|
|
596
|
+
String(config.useTrickleIce ?? false)
|
|
597
|
+
);
|
|
598
|
+
await AsyncStorage.setItem(
|
|
599
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
600
|
+
String(config.enableMissedCallNotifications ?? false)
|
|
601
|
+
);
|
|
566
602
|
|
|
567
603
|
if (config.pushNotificationDeviceToken) {
|
|
568
604
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
@@ -14,6 +14,14 @@ declare module '@telnyx/react-native-voice-sdk' {
|
|
|
14
14
|
destinationNumber?: string;
|
|
15
15
|
audio?: boolean;
|
|
16
16
|
video?: boolean;
|
|
17
|
+
peerConnectionOptions?: {
|
|
18
|
+
prefetchIceCandidates?: boolean;
|
|
19
|
+
useTrickleIce?: boolean;
|
|
20
|
+
iceServers?: any[];
|
|
21
|
+
iceTransportPolicy?: string;
|
|
22
|
+
bundlePolicy?: string;
|
|
23
|
+
rtcpMuxPolicy?: string;
|
|
24
|
+
};
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface ClientOptions {
|
|
@@ -26,6 +34,8 @@ declare module '@telnyx/react-native-voice-sdk' {
|
|
|
26
34
|
debug?: boolean;
|
|
27
35
|
logLevel?: string;
|
|
28
36
|
pushNotificationDeviceToken?: string;
|
|
37
|
+
enableMissedCallNotifications?: boolean;
|
|
38
|
+
useTrickleIce?: boolean;
|
|
29
39
|
enableCallReports?: boolean;
|
|
30
40
|
callReportInterval?: number;
|
|
31
41
|
callReportLogLevel?: string;
|