@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.
@@ -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
- if (!this._sessionManager.telnyxClient) {
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
- this._sessionManager.telnyxClient.on('telnyx.call.incoming', (telnyxCall, msg) => {
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
- this._sessionManager.telnyxClient.on('telnyx.call.reattached', (telnyxCall, msg) => {
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
- this._sessionManager.telnyxClient.listenerCount('telnyx.call.incoming');
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
- this._sessionManager.telnyxClient.on('telnyx.call.stateChanged', (telnyxCall, state) => {
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
- this._sessionManager.telnyxClient.on('telnyx.call.removed', (callId) => {
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
  */
@@ -42,6 +42,7 @@ export declare class SessionManager {
42
42
  * Get the underlying Telnyx client instance
43
43
  */
44
44
  get telnyxClient(): TelnyxSDK.TelnyxRTC | undefined;
45
+ get useTrickleIce(): boolean;
45
46
  /**
46
47
  * Connect using credential authentication
47
48
  */
@@ -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
- // If we have a TelnyxRTC instance but its socket isn't fresh — either not
206
- // connected at all, or connected but with no traffic for longer than the
207
- // threshold — dispose it so we go through the full _connect() path below
208
- // instead of calling processVoIPNotification on a stale client.
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
- const client = this._telnyxClient;
222
- const isFresh =
223
- typeof client.isFresh === 'function'
224
- ? client.isFresh(STALE_THRESHOLD_MS)
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)
@@ -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(config);
190
- await this._sessionManager.connectWithCredential(config);
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(config);
212
- await this._sessionManager.connectWithToken(config);
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
- // First, pass the push notification to the session manager for processing
337
- // This will set the isCallFromPush flag on the TelnyxRTC client
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.4.2",
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.4.4",
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';