@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
package/ios/VoicePnBridge.swift
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import React
|
|
3
|
+
import AVFoundation
|
|
3
4
|
|
|
4
5
|
@objc(VoicePnBridge)
|
|
5
6
|
class VoicePnBridge: NSObject {
|
|
@@ -84,4 +85,49 @@ class VoicePnBridge: NSObject {
|
|
|
84
85
|
NSLog("[VoicePnBridge] clearPendingVoipAction called. Cleared key pending_voip_action")
|
|
85
86
|
resolve(true)
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
|
|
89
|
+
@objc
|
|
90
|
+
func setMissedCallNotificationsEnabled(_ enabled: Bool, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
91
|
+
UserDefaults.standard.set(enabled, forKey: TelnyxVoiceUserDefaultsKey.missedCallNotificationsEnabled)
|
|
92
|
+
UserDefaults.standard.synchronize()
|
|
93
|
+
NSLog("[VoicePnBridge] setMissedCallNotificationsEnabled called. enabled=\(enabled)")
|
|
94
|
+
resolve(true)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@objc
|
|
98
|
+
func getMissedCallNotificationsEnabled(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
99
|
+
if UserDefaults.standard.object(forKey: TelnyxVoiceUserDefaultsKey.missedCallNotificationsEnabled) == nil {
|
|
100
|
+
resolve(true)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
resolve(UserDefaults.standard.bool(forKey: TelnyxVoiceUserDefaultsKey.missedCallNotificationsEnabled))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc
|
|
108
|
+
func setSpeakerEnabled(_ enabled: Bool, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
109
|
+
do {
|
|
110
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
111
|
+
// The Telnyx/WebRTC call setup is expected to configure a .playAndRecord-compatible
|
|
112
|
+
// session. Passing .none clears only the speaker override; accessories/session options
|
|
113
|
+
// can still decide the resulting output route.
|
|
114
|
+
try audioSession.overrideOutputAudioPort(enabled ? .speaker : .none)
|
|
115
|
+
let actualEnabled = audioSession.currentRoute.outputs.contains { output in
|
|
116
|
+
output.portType == .builtInSpeaker
|
|
117
|
+
}
|
|
118
|
+
NSLog("[VoicePnBridge] setSpeakerEnabled called. requested=\(enabled), actual=\(actualEnabled)")
|
|
119
|
+
resolve(actualEnabled)
|
|
120
|
+
} catch {
|
|
121
|
+
NSLog("[VoicePnBridge] setSpeakerEnabled failed. enabled=\(enabled), error=\(error)")
|
|
122
|
+
reject("SET_SPEAKER_ENABLED_ERROR", error.localizedDescription, error)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@objc
|
|
127
|
+
func isSpeakerEnabled(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
128
|
+
let isEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.contains { output in
|
|
129
|
+
output.portType == .builtInSpeaker
|
|
130
|
+
}
|
|
131
|
+
resolve(isEnabled)
|
|
132
|
+
}
|
|
133
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -41,4 +41,5 @@ export {
|
|
|
41
41
|
export type { Config, CredentialConfig, TokenConfig } from './models/config';
|
|
42
42
|
export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
43
43
|
export * from './callkit';
|
|
44
|
+
export { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
44
45
|
export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var __exportStar =
|
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
40
40
|
exports.useAppReadyNotifier =
|
|
41
|
+
exports.VoicePnBridge =
|
|
41
42
|
exports.createTokenConfig =
|
|
42
43
|
exports.createCredentialConfig =
|
|
43
44
|
exports.validateTokenConfig =
|
|
@@ -215,6 +216,14 @@ Object.defineProperty(exports, 'createTokenConfig', {
|
|
|
215
216
|
});
|
|
216
217
|
// Export CallKit functionality
|
|
217
218
|
__exportStar(require('./callkit'), exports);
|
|
219
|
+
// Native push notification helpers
|
|
220
|
+
var voice_pn_bridge_1 = require('./internal/voice-pn-bridge');
|
|
221
|
+
Object.defineProperty(exports, 'VoicePnBridge', {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
get: function () {
|
|
224
|
+
return voice_pn_bridge_1.VoicePnBridge;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
218
227
|
// Export hooks
|
|
219
228
|
var useAppReadyNotifier_1 = require('./hooks/useAppReadyNotifier');
|
|
220
229
|
Object.defineProperty(exports, 'useAppReadyNotifier', {
|
|
@@ -12,6 +12,7 @@ export declare class CallStateController {
|
|
|
12
12
|
private readonly _calls;
|
|
13
13
|
private readonly _callMap;
|
|
14
14
|
private _disposed;
|
|
15
|
+
private _listenerClient?;
|
|
15
16
|
private _isWaitingForInvite?;
|
|
16
17
|
private _onInviteAutoAccepted?;
|
|
17
18
|
constructor(_sessionManager: SessionManager);
|
|
@@ -86,6 +87,11 @@ export declare class CallStateController {
|
|
|
86
87
|
* Set up event listeners for the Telnyx client
|
|
87
88
|
*/
|
|
88
89
|
private _setupClientListeners;
|
|
90
|
+
private _removeClientListeners;
|
|
91
|
+
private _handleTelnyxIncomingCall;
|
|
92
|
+
private _handleTelnyxReattachedCall;
|
|
93
|
+
private _handleTelnyxCallStateChanged;
|
|
94
|
+
private _handleTelnyxCallRemoved;
|
|
89
95
|
/**
|
|
90
96
|
* Handle incoming call or reattached call
|
|
91
97
|
*/
|
|
@@ -19,6 +19,33 @@ class CallStateController {
|
|
|
19
19
|
this._calls = new rxjs_1.BehaviorSubject([]);
|
|
20
20
|
this._callMap = new Map();
|
|
21
21
|
this._disposed = false;
|
|
22
|
+
this._handleTelnyxIncomingCall = (telnyxCall, msg) => {
|
|
23
|
+
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
24
|
+
this._handleIncomingCall(telnyxCall, msg, false);
|
|
25
|
+
};
|
|
26
|
+
this._handleTelnyxReattachedCall = (telnyxCall, msg) => {
|
|
27
|
+
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
28
|
+
this._handleIncomingCall(telnyxCall, msg, true);
|
|
29
|
+
};
|
|
30
|
+
this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
|
|
31
|
+
console.log(
|
|
32
|
+
'CallStateController: Call state changed from TelnyxRTC:',
|
|
33
|
+
telnyxCall.callId,
|
|
34
|
+
state
|
|
35
|
+
);
|
|
36
|
+
// Find our wrapper call and update if needed
|
|
37
|
+
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
38
|
+
if (call) {
|
|
39
|
+
console.log(
|
|
40
|
+
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
this._handleTelnyxCallRemoved = (callId) => {
|
|
45
|
+
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
46
|
+
// The call cleanup is already handled by our call state subscription.
|
|
47
|
+
// This event is informational for logging/debugging.
|
|
48
|
+
};
|
|
22
49
|
console.log('CallStateController: Constructor called - instance created');
|
|
23
50
|
// Don't set up client listeners here - client doesn't exist yet
|
|
24
51
|
// Will be called when client is available
|
|
@@ -134,6 +161,9 @@ class CallStateController {
|
|
|
134
161
|
callerIdName: callerName,
|
|
135
162
|
callerIdNumber: callerNumber,
|
|
136
163
|
customHeaders,
|
|
164
|
+
peerConnectionOptions: {
|
|
165
|
+
useTrickleIce: this._sessionManager.useTrickleIce,
|
|
166
|
+
},
|
|
137
167
|
};
|
|
138
168
|
const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
|
|
139
169
|
// Create our wrapper Call object
|
|
@@ -192,6 +222,7 @@ class CallStateController {
|
|
|
192
222
|
return;
|
|
193
223
|
}
|
|
194
224
|
this._disposed = true;
|
|
225
|
+
this._removeClientListeners();
|
|
195
226
|
// Dispose of all calls
|
|
196
227
|
this.currentCalls.forEach((call) => call.dispose());
|
|
197
228
|
this._callMap.clear();
|
|
@@ -203,30 +234,22 @@ class CallStateController {
|
|
|
203
234
|
*/
|
|
204
235
|
_setupClientListeners() {
|
|
205
236
|
console.log('CallStateController: Setting up client listeners...');
|
|
206
|
-
|
|
237
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
238
|
+
if (!telnyxClient) {
|
|
207
239
|
console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
|
|
208
240
|
return;
|
|
209
241
|
}
|
|
242
|
+
this._removeClientListeners();
|
|
243
|
+
this._listenerClient = telnyxClient;
|
|
210
244
|
console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
|
|
211
|
-
console.log(
|
|
212
|
-
'CallStateController: Client instance:',
|
|
213
|
-
this._sessionManager.telnyxClient.constructor.name
|
|
214
|
-
);
|
|
245
|
+
console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
|
|
215
246
|
// Listen for incoming calls
|
|
216
|
-
|
|
217
|
-
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
218
|
-
this._handleIncomingCall(telnyxCall, msg, false);
|
|
219
|
-
});
|
|
247
|
+
telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
220
248
|
// Listen for reattached calls (after network reconnection)
|
|
221
|
-
|
|
222
|
-
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
223
|
-
this._handleIncomingCall(telnyxCall, msg, true);
|
|
224
|
-
});
|
|
249
|
+
telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
225
250
|
// Verify listeners are set up
|
|
226
|
-
const incomingListeners =
|
|
227
|
-
|
|
228
|
-
const reattachedListeners =
|
|
229
|
-
this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
|
|
251
|
+
const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
|
|
252
|
+
const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
|
|
230
253
|
console.log(
|
|
231
254
|
'CallStateController: Listeners registered - incoming:',
|
|
232
255
|
incomingListeners,
|
|
@@ -234,28 +257,21 @@ class CallStateController {
|
|
|
234
257
|
reattachedListeners
|
|
235
258
|
);
|
|
236
259
|
// Listen for call state changes from the TelnyxRTC client (multi-call support)
|
|
237
|
-
|
|
238
|
-
console.log(
|
|
239
|
-
'CallStateController: Call state changed from TelnyxRTC:',
|
|
240
|
-
telnyxCall.callId,
|
|
241
|
-
state
|
|
242
|
-
);
|
|
243
|
-
// Find our wrapper call and update if needed
|
|
244
|
-
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
245
|
-
if (call) {
|
|
246
|
-
console.log(
|
|
247
|
-
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
260
|
+
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
251
261
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
252
|
-
|
|
253
|
-
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
254
|
-
// The call cleanup is already handled by our call state subscription
|
|
255
|
-
// This event is informational for logging/debugging
|
|
256
|
-
});
|
|
262
|
+
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
257
263
|
console.log('CallStateController: Client listeners set up successfully');
|
|
258
264
|
}
|
|
265
|
+
_removeClientListeners() {
|
|
266
|
+
if (!this._listenerClient) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
270
|
+
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
271
|
+
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
272
|
+
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
273
|
+
this._listenerClient = undefined;
|
|
274
|
+
}
|
|
259
275
|
/**
|
|
260
276
|
* Handle incoming call or reattached call
|
|
261
277
|
*/
|
|
@@ -59,6 +59,8 @@ 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 MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
62
64
|
/**
|
|
63
65
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
64
66
|
*
|
|
@@ -110,6 +112,9 @@ class SessionManager {
|
|
|
110
112
|
get telnyxClient() {
|
|
111
113
|
return this._telnyxClient;
|
|
112
114
|
}
|
|
115
|
+
get useTrickleIce() {
|
|
116
|
+
return Boolean(this._currentConfig?.useTrickleIce);
|
|
117
|
+
}
|
|
113
118
|
/**
|
|
114
119
|
* Connect using credential authentication
|
|
115
120
|
*/
|
|
@@ -202,39 +207,22 @@ class SessionManager {
|
|
|
202
207
|
);
|
|
203
208
|
// Store the push notification payload for when the client is created
|
|
204
209
|
this._pendingPushPayload = payload;
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// This handles two cases iOS gives us with the same code path:
|
|
211
|
-
// 1. Terminated-then-cold-launched: a client exists (constructed at
|
|
212
|
-
// app boot) but its connection was never opened (idleMs=Infinity).
|
|
213
|
-
// 2. Suspended-then-thawed: a client exists with `connected=true`,
|
|
214
|
-
// but the kernel killed the TLS session during freeze without
|
|
215
|
-
// notifying the JS WebSocket wrapper, so no error event fired.
|
|
216
|
-
//
|
|
217
|
-
// 30s threshold matches the server's keep-alive ping cadence (~20s),
|
|
218
|
-
// so a live session always remains fresh.
|
|
219
|
-
const STALE_THRESHOLD_MS = 30000;
|
|
210
|
+
// Each push always rebuilds the TelnyxRTC client. The voice_sdk_id is
|
|
211
|
+
// baked into the WebSocket URL at socket-open time and can't be mutated
|
|
212
|
+
// mid-flight; reusing a connection from a previous push means the
|
|
213
|
+
// gateway routes THIS push's INVITE to a different (correctly-stamped)
|
|
214
|
+
// client and we sit on the wrong socket forever.
|
|
220
215
|
if (this._telnyxClient) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: !!client.connected;
|
|
226
|
-
if (!isFresh) {
|
|
227
|
-
try {
|
|
228
|
-
await this._telnyxClient.disconnect();
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.warn('SessionManager: disconnect of stale client threw:', err);
|
|
231
|
-
}
|
|
232
|
-
this._telnyxClient = undefined;
|
|
233
|
-
if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
|
|
234
|
-
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
235
|
-
}
|
|
216
|
+
try {
|
|
217
|
+
await this._telnyxClient.disconnect();
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn('SessionManager: disconnect of prior client threw:', err);
|
|
236
220
|
}
|
|
237
221
|
}
|
|
222
|
+
this._telnyxClient = undefined;
|
|
223
|
+
if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
|
|
224
|
+
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
225
|
+
}
|
|
238
226
|
// If we don't have a config yet but we're processing a push notification,
|
|
239
227
|
// attempt to load stored config first (for terminated app startup)
|
|
240
228
|
if (!this._currentConfig && !this._telnyxClient) {
|
|
@@ -248,12 +236,20 @@ class SessionManager {
|
|
|
248
236
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
249
237
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
250
238
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
239
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
240
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
241
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
242
|
+
);
|
|
243
|
+
const useTrickleIce = storedUseTrickleIce === 'true';
|
|
244
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
251
245
|
// Check if we have credential-based authentication data
|
|
252
246
|
if (storedUsername && storedPassword) {
|
|
253
247
|
console.log('SessionManager: RELEASE DEBUG - Found stored credentials, creating config');
|
|
254
248
|
const { createCredentialConfig } = require('../../models/config');
|
|
255
249
|
this._currentConfig = createCredentialConfig(storedUsername, storedPassword, {
|
|
256
250
|
pushNotificationDeviceToken: storedPushToken,
|
|
251
|
+
useTrickleIce,
|
|
252
|
+
enableMissedCallNotifications,
|
|
257
253
|
});
|
|
258
254
|
}
|
|
259
255
|
// Check if we have token-based authentication data
|
|
@@ -262,6 +258,8 @@ class SessionManager {
|
|
|
262
258
|
const { createTokenConfig } = require('../../models/config');
|
|
263
259
|
this._currentConfig = createTokenConfig(storedCredentialToken, {
|
|
264
260
|
pushNotificationDeviceToken: storedPushToken,
|
|
261
|
+
useTrickleIce,
|
|
262
|
+
enableMissedCallNotifications,
|
|
265
263
|
});
|
|
266
264
|
}
|
|
267
265
|
if (this._currentConfig) {
|
|
@@ -375,6 +373,8 @@ class SessionManager {
|
|
|
375
373
|
logLevel: this._currentConfig.debug ? 'debug' : 'warn',
|
|
376
374
|
debug: this._currentConfig.debug ?? false,
|
|
377
375
|
pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
|
|
376
|
+
enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
|
|
377
|
+
useTrickleIce: this._currentConfig.useTrickleIce,
|
|
378
378
|
enableCallReports: this._currentConfig.enableCallReports,
|
|
379
379
|
callReportInterval: this._currentConfig.callReportInterval,
|
|
380
380
|
callReportLogLevel: this._currentConfig.callReportLogLevel,
|
|
@@ -393,6 +393,8 @@ class SessionManager {
|
|
|
393
393
|
logLevel: this._currentConfig.debug ? 'debug' : 'warn',
|
|
394
394
|
debug: this._currentConfig.debug ?? false,
|
|
395
395
|
pushNotificationDeviceToken: this._currentConfig.pushNotificationDeviceToken,
|
|
396
|
+
enableMissedCallNotifications: this._currentConfig.enableMissedCallNotifications ?? false,
|
|
397
|
+
useTrickleIce: this._currentConfig.useTrickleIce,
|
|
396
398
|
enableCallReports: this._currentConfig.enableCallReports,
|
|
397
399
|
callReportInterval: this._currentConfig.callReportInterval,
|
|
398
400
|
callReportLogLevel: this._currentConfig.callReportLogLevel,
|
|
@@ -30,6 +30,10 @@ export interface VoicePnBridgeInterface {
|
|
|
30
30
|
clearPendingVoipPush(): Promise<boolean>;
|
|
31
31
|
getPendingVoipAction(): Promise<string | null>;
|
|
32
32
|
clearPendingVoipAction(): Promise<boolean>;
|
|
33
|
+
setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
34
|
+
getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
35
|
+
setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
36
|
+
isSpeakerEnabled(): Promise<boolean>;
|
|
33
37
|
}
|
|
34
38
|
declare const NativeBridge: VoicePnBridgeInterface;
|
|
35
39
|
/**
|
|
@@ -107,6 +111,28 @@ export declare class VoicePnBridge {
|
|
|
107
111
|
* Clear pending VoIP action from native storage (iOS only)
|
|
108
112
|
*/
|
|
109
113
|
static clearPendingVoipAction(): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* Enable or disable local missed call notifications on iOS.
|
|
116
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
117
|
+
*/
|
|
118
|
+
static setMissedCallNotificationsEnabled(enabled: boolean): Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
121
|
+
*/
|
|
122
|
+
static getMissedCallNotificationsEnabled(): Promise<boolean>;
|
|
123
|
+
/**
|
|
124
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
125
|
+
* platform default voice route when disabled.
|
|
126
|
+
*/
|
|
127
|
+
static setSpeakerEnabled(enabled: boolean): Promise<boolean>;
|
|
128
|
+
/**
|
|
129
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
130
|
+
*/
|
|
131
|
+
static isSpeakerEnabled(): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
134
|
+
*/
|
|
135
|
+
static toggleSpeaker(): Promise<boolean>;
|
|
110
136
|
/**
|
|
111
137
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
112
138
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|
|
@@ -184,6 +184,61 @@ class VoicePnBridge {
|
|
|
184
184
|
return false;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Enable or disable local missed call notifications on iOS.
|
|
189
|
+
* Missed-call VoIP pushes are still handled through CallKit either way.
|
|
190
|
+
*/
|
|
191
|
+
static async setMissedCallNotificationsEnabled(enabled) {
|
|
192
|
+
try {
|
|
193
|
+
return await NativeBridge.setMissedCallNotificationsEnabled(enabled);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('VoicePnBridge: Error setting missed call notifications:', error);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns true by default, matching the sample-app behavior for missed calls.
|
|
201
|
+
*/
|
|
202
|
+
static async getMissedCallNotificationsEnabled() {
|
|
203
|
+
try {
|
|
204
|
+
return await NativeBridge.getMissedCallNotificationsEnabled();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('VoicePnBridge: Error getting missed call notifications setting:', error);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Route call audio through the loudspeaker when enabled, or back to the
|
|
212
|
+
* platform default voice route when disabled.
|
|
213
|
+
*/
|
|
214
|
+
static async setSpeakerEnabled(enabled) {
|
|
215
|
+
try {
|
|
216
|
+
return await NativeBridge.setSpeakerEnabled(enabled);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error('VoicePnBridge: Error setting speaker route:', error);
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Returns whether the current audio route is using the loudspeaker.
|
|
224
|
+
*/
|
|
225
|
+
static async isSpeakerEnabled() {
|
|
226
|
+
try {
|
|
227
|
+
return await NativeBridge.isSpeakerEnabled();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('VoicePnBridge: Error getting speaker route:', error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Toggle between loudspeaker and the platform default voice route.
|
|
235
|
+
*/
|
|
236
|
+
static async toggleSpeaker() {
|
|
237
|
+
// This is intentionally read-then-write because route changes can also come from
|
|
238
|
+
// the OS, Bluetooth, wired accessories, or the active call session.
|
|
239
|
+
const enabled = await VoicePnBridge.isSpeakerEnabled();
|
|
240
|
+
return VoicePnBridge.setSpeakerEnabled(!enabled);
|
|
241
|
+
}
|
|
187
242
|
/**
|
|
188
243
|
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
189
244
|
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|
package/lib/models/config.d.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 */
|
|
@@ -24,6 +28,10 @@ export interface TokenConfig {
|
|
|
24
28
|
token: string;
|
|
25
29
|
debug?: boolean;
|
|
26
30
|
pushNotificationDeviceToken?: string;
|
|
31
|
+
/** Enable native missed call push notifications. Default: false */
|
|
32
|
+
enableMissedCallNotifications?: boolean;
|
|
33
|
+
/** Enable Trickle ICE. Default: false */
|
|
34
|
+
useTrickleIce?: boolean;
|
|
27
35
|
/** Enable automatic call quality reporting. Default: true */
|
|
28
36
|
enableCallReports?: boolean;
|
|
29
37
|
/** Stats collection interval in seconds. Default: 5 */
|
|
@@ -10,6 +10,8 @@ export interface TelnyxVoipClientOptions {
|
|
|
10
10
|
enableAppStateManagement?: boolean;
|
|
11
11
|
/** Enable debug logging */
|
|
12
12
|
debug?: boolean;
|
|
13
|
+
/** Enable Trickle ICE for calls created by this client */
|
|
14
|
+
useTrickleIce?: boolean;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* The main public interface for the react-voice-commons module.
|
|
@@ -10,6 +10,8 @@ const config_1 = require('./models/config');
|
|
|
10
10
|
const session_manager_1 = require('./internal/session/session-manager');
|
|
11
11
|
const call_state_controller_1 = require('./internal/calls/call-state-controller');
|
|
12
12
|
const voice_pn_bridge_1 = require('./internal/voice-pn-bridge');
|
|
13
|
+
const USE_TRICKLE_ICE_STORAGE_KEY = '@use_trickle_ice';
|
|
14
|
+
const MISSED_CALL_NOTIFICATIONS_STORAGE_KEY = '@enable_missed_call_notifications';
|
|
13
15
|
/**
|
|
14
16
|
* The main public interface for the react-voice-commons module.
|
|
15
17
|
*
|
|
@@ -52,6 +54,7 @@ class TelnyxVoipClient {
|
|
|
52
54
|
this._options = {
|
|
53
55
|
enableAppStateManagement: true,
|
|
54
56
|
debug: false,
|
|
57
|
+
useTrickleIce: false,
|
|
55
58
|
...options,
|
|
56
59
|
};
|
|
57
60
|
// Initialize core components
|
|
@@ -185,9 +188,13 @@ class TelnyxVoipClient {
|
|
|
185
188
|
if (this._options.debug) {
|
|
186
189
|
console.log('TelnyxVoipClient: Logging in with credentials for user:', config.sipUser);
|
|
187
190
|
}
|
|
191
|
+
const loginConfig = {
|
|
192
|
+
...config,
|
|
193
|
+
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
194
|
+
};
|
|
188
195
|
// Store credentials for future reconnection
|
|
189
|
-
await this._storeCredentials(
|
|
190
|
-
await this._sessionManager.connectWithCredential(
|
|
196
|
+
await this._storeCredentials(loginConfig);
|
|
197
|
+
await this._sessionManager.connectWithCredential(loginConfig);
|
|
191
198
|
}
|
|
192
199
|
/**
|
|
193
200
|
* Connects to the Telnyx platform using token authentication.
|
|
@@ -207,9 +214,13 @@ class TelnyxVoipClient {
|
|
|
207
214
|
if (this._options.debug) {
|
|
208
215
|
console.log('TelnyxVoipClient: Logging in with token');
|
|
209
216
|
}
|
|
217
|
+
const loginConfig = {
|
|
218
|
+
...config,
|
|
219
|
+
useTrickleIce: config.useTrickleIce ?? this._options.useTrickleIce,
|
|
220
|
+
};
|
|
210
221
|
// Store token for future reconnection
|
|
211
|
-
await this._storeToken(
|
|
212
|
-
await this._sessionManager.connectWithToken(
|
|
222
|
+
await this._storeToken(loginConfig);
|
|
223
|
+
await this._sessionManager.connectWithToken(loginConfig);
|
|
213
224
|
}
|
|
214
225
|
/**
|
|
215
226
|
* Disconnects from the Telnyx platform.
|
|
@@ -246,12 +257,21 @@ class TelnyxVoipClient {
|
|
|
246
257
|
const storedPassword = await AsyncStorage.getItem('@telnyx_password');
|
|
247
258
|
const storedCredentialToken = await AsyncStorage.getItem('@credential_token');
|
|
248
259
|
const storedPushToken = await AsyncStorage.getItem('@push_token');
|
|
260
|
+
const storedUseTrickleIce = await AsyncStorage.getItem(USE_TRICKLE_ICE_STORAGE_KEY);
|
|
261
|
+
const storedMissedCallNotifications = await AsyncStorage.getItem(
|
|
262
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY
|
|
263
|
+
);
|
|
264
|
+
const useTrickleIce =
|
|
265
|
+
storedUseTrickleIce === null ? this._options.useTrickleIce : storedUseTrickleIce === 'true';
|
|
266
|
+
const enableMissedCallNotifications = storedMissedCallNotifications === 'true';
|
|
249
267
|
// Check if we have credential-based authentication data
|
|
250
268
|
if (storedUsername && storedPassword) {
|
|
251
269
|
// Create credential config from stored data
|
|
252
270
|
const { createCredentialConfig } = require('./models/config');
|
|
253
271
|
const config = createCredentialConfig(storedUsername, storedPassword, {
|
|
254
272
|
pushNotificationDeviceToken: storedPushToken,
|
|
273
|
+
useTrickleIce,
|
|
274
|
+
enableMissedCallNotifications,
|
|
255
275
|
});
|
|
256
276
|
if (this._options.debug) {
|
|
257
277
|
console.log(
|
|
@@ -268,6 +288,8 @@ class TelnyxVoipClient {
|
|
|
268
288
|
const { createTokenConfig } = require('./models/config');
|
|
269
289
|
const config = createTokenConfig(storedCredentialToken, {
|
|
270
290
|
pushNotificationDeviceToken: storedPushToken,
|
|
291
|
+
useTrickleIce,
|
|
292
|
+
enableMissedCallNotifications,
|
|
271
293
|
});
|
|
272
294
|
if (this._options.debug) {
|
|
273
295
|
console.log('TelnyxVoipClient: Reconnecting with stored token');
|
|
@@ -333,21 +355,14 @@ class TelnyxVoipClient {
|
|
|
333
355
|
console.log('TelnyxVoipClient: Handling push notification:', payload);
|
|
334
356
|
}
|
|
335
357
|
try {
|
|
336
|
-
//
|
|
337
|
-
//
|
|
358
|
+
// SessionManager owns the entire push lifecycle: it disposes any prior
|
|
359
|
+
// client, loads stored credentials if needed, and rebuilds the
|
|
360
|
+
// TelnyxRTC bound to THIS push's voice_sdk_id. We deliberately do not
|
|
361
|
+
// fall back to a generic loginFromStoredConfig() here — that would
|
|
362
|
+
// create a parallel client with no voice_sdk_id awareness, which the
|
|
363
|
+
// gateway then has to `punt` once the SessionManager-driven session
|
|
364
|
+
// also registers, dropping the active call.
|
|
338
365
|
await this._sessionManager.handlePushNotification(payload);
|
|
339
|
-
// Connect if not already connected
|
|
340
|
-
const currentState = this.currentConnectionState;
|
|
341
|
-
if (currentState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
|
|
342
|
-
// Try to login from stored config - now the push flags should be set
|
|
343
|
-
const loginSuccess = await this.loginFromStoredConfig();
|
|
344
|
-
if (!loginSuccess) {
|
|
345
|
-
console.warn(
|
|
346
|
-
'TelnyxVoipClient: Could not login from stored config for push notification'
|
|
347
|
-
);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
366
|
} catch (error) {
|
|
352
367
|
console.error('TelnyxVoipClient: Error handling push notification:', error);
|
|
353
368
|
throw error;
|
|
@@ -448,6 +463,14 @@ class TelnyxVoipClient {
|
|
|
448
463
|
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
449
464
|
await AsyncStorage.setItem('@telnyx_username', config.sipUser);
|
|
450
465
|
await AsyncStorage.setItem('@telnyx_password', config.sipPassword);
|
|
466
|
+
await AsyncStorage.setItem(
|
|
467
|
+
USE_TRICKLE_ICE_STORAGE_KEY,
|
|
468
|
+
String(config.useTrickleIce ?? false)
|
|
469
|
+
);
|
|
470
|
+
await AsyncStorage.setItem(
|
|
471
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
472
|
+
String(config.enableMissedCallNotifications ?? false)
|
|
473
|
+
);
|
|
451
474
|
if (config.pushNotificationDeviceToken) {
|
|
452
475
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
453
476
|
}
|
|
@@ -470,6 +493,14 @@ class TelnyxVoipClient {
|
|
|
470
493
|
try {
|
|
471
494
|
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
472
495
|
await AsyncStorage.setItem('@credential_token', config.token);
|
|
496
|
+
await AsyncStorage.setItem(
|
|
497
|
+
USE_TRICKLE_ICE_STORAGE_KEY,
|
|
498
|
+
String(config.useTrickleIce ?? false)
|
|
499
|
+
);
|
|
500
|
+
await AsyncStorage.setItem(
|
|
501
|
+
MISSED_CALL_NOTIFICATIONS_STORAGE_KEY,
|
|
502
|
+
String(config.enableMissedCallNotifications ?? false)
|
|
503
|
+
);
|
|
473
504
|
if (config.pushNotificationDeviceToken) {
|
|
474
505
|
await AsyncStorage.setItem('@push_token', config.pushNotificationDeviceToken);
|
|
475
506
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
89
|
-
"@telnyx/react-native-voice-sdk": ">=0.
|
|
89
|
+
"@telnyx/react-native-voice-sdk": ">=1.0.0",
|
|
90
90
|
"eventemitter3": "^5.0.1",
|
|
91
91
|
"expo": "~53.0.22",
|
|
92
92
|
"react-native-url-polyfill": "^3.0.0",
|
package/src/index.ts
CHANGED
|
@@ -55,5 +55,8 @@ export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
|
55
55
|
// Export CallKit functionality
|
|
56
56
|
export * from './callkit';
|
|
57
57
|
|
|
58
|
+
// Native push notification helpers
|
|
59
|
+
export { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
60
|
+
|
|
58
61
|
// Export hooks
|
|
59
62
|
export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
|