@telnyx/react-voice-commons-sdk 0.4.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -7
  3. package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
  4. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
  5. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
  6. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
  7. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
  8. package/ios/CallKitBridge.m +20 -1
  9. package/ios/CallKitBridge.swift +935 -60
  10. package/ios/README.md +2 -2
  11. package/ios/VoicePnBridge.m +21 -1
  12. package/ios/VoicePnBridge.swift +62 -1
  13. package/lib/callkit/callkit-coordinator.d.ts +39 -3
  14. package/lib/callkit/callkit-coordinator.js +561 -148
  15. package/lib/callkit/callkit.d.ts +7 -0
  16. package/lib/callkit/callkit.js +83 -11
  17. package/lib/hooks/useAppStateHandler.js +6 -2
  18. package/lib/index.d.ts +1 -0
  19. package/lib/index.js +9 -0
  20. package/lib/internal/CallKitHandler.d.ts +3 -0
  21. package/lib/internal/CallKitHandler.js +52 -46
  22. package/lib/internal/calls/call-state-controller.d.ts +39 -1
  23. package/lib/internal/calls/call-state-controller.js +152 -64
  24. package/lib/internal/session/session-manager.d.ts +16 -1
  25. package/lib/internal/session/session-manager.js +229 -58
  26. package/lib/internal/voice-pn-bridge.d.ts +44 -0
  27. package/lib/internal/voice-pn-bridge.js +94 -0
  28. package/lib/models/call.js +35 -0
  29. package/lib/models/config.d.ts +18 -0
  30. package/lib/models/config.js +2 -0
  31. package/lib/telnyx-voice-app.js +85 -43
  32. package/lib/telnyx-voip-client.d.ts +48 -5
  33. package/lib/telnyx-voip-client.js +276 -22
  34. package/package.json +7 -5
  35. package/src/callkit/callkit-coordinator.ts +642 -160
  36. package/src/callkit/callkit.ts +110 -12
  37. package/src/hooks/useAppStateHandler.ts +7 -2
  38. package/src/index.ts +3 -0
  39. package/src/internal/CallKitHandler.tsx +60 -55
  40. package/src/internal/calls/call-state-controller.ts +173 -74
  41. package/src/internal/session/session-manager.ts +277 -61
  42. package/src/internal/voice-pn-bridge.ts +111 -0
  43. package/src/models/call.ts +34 -0
  44. package/src/models/config.ts +18 -0
  45. package/src/telnyx-voice-app.tsx +96 -45
  46. package/src/telnyx-voip-client.ts +260 -24
  47. package/src/types/telnyx-sdk.d.ts +12 -1
@@ -6,7 +6,6 @@ const operators_1 = require('rxjs/operators');
6
6
  const call_1 = require('../../models/call');
7
7
  const call_state_1 = require('../../models/call-state');
8
8
  const callkit_coordinator_1 = require('../../callkit/callkit-coordinator');
9
- const voice_pn_bridge_1 = require('../voice-pn-bridge');
10
9
  /**
11
10
  * Central state machine for call management.
12
11
  *
@@ -19,6 +18,38 @@ class CallStateController {
19
18
  this._calls = new rxjs_1.BehaviorSubject([]);
20
19
  this._callMap = new Map();
21
20
  this._disposed = false;
21
+ // Explicitly-tracked active call ID for multi-call scenarios.
22
+ // When set, activeCall$/currentActiveCall prefer this call over the
23
+ // first-match heuristic. Cleared automatically when the call reaches
24
+ // a terminal state, or manually via clearActiveCall().
25
+ this._activeCallId = null;
26
+ this._handleTelnyxIncomingCall = (telnyxCall, msg) => {
27
+ console.log('CallStateController: Incoming call received:', telnyxCall.callId);
28
+ this._handleIncomingCall(telnyxCall, msg, false);
29
+ };
30
+ this._handleTelnyxReattachedCall = (telnyxCall, msg) => {
31
+ console.log('CallStateController: Reattached call received:', telnyxCall.callId);
32
+ this._handleIncomingCall(telnyxCall, msg, true);
33
+ };
34
+ this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
35
+ console.log(
36
+ 'CallStateController: Call state changed from TelnyxRTC:',
37
+ telnyxCall.callId,
38
+ state
39
+ );
40
+ // Find our wrapper call and update if needed
41
+ const call = this.findCallByTelnyxCall(telnyxCall);
42
+ if (call) {
43
+ console.log(
44
+ 'CallStateController: Found wrapper call, state sync handled by Call subscription'
45
+ );
46
+ }
47
+ };
48
+ this._handleTelnyxCallRemoved = (callId) => {
49
+ console.log('CallStateController: Call removed from TelnyxRTC:', callId);
50
+ // The call cleanup is already handled by our call state subscription.
51
+ // This event is informational for logging/debugging.
52
+ };
22
53
  console.log('CallStateController: Constructor called - instance created');
23
54
  // Don't set up client listeners here - client doesn't exist yet
24
55
  // Will be called when client is available
@@ -35,18 +66,18 @@ class CallStateController {
35
66
  get activeCall$() {
36
67
  return this.calls$.pipe(
37
68
  (0, operators_1.map)((calls) => {
38
- // Find the first call that is not terminated (includes RINGING, CONNECTING, ACTIVE, HELD)
39
- return (
40
- calls.find(
41
- (call) =>
42
- call.currentState === call_state_1.TelnyxCallState.RINGING ||
43
- call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
44
- call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
45
- call.currentState === call_state_1.TelnyxCallState.HELD
46
- ) || null
47
- );
69
+ const call = this._selectActiveCall(calls);
70
+ return {
71
+ call,
72
+ callId: call?.callId ?? null,
73
+ state: call?.currentState ?? null,
74
+ };
48
75
  }),
49
- (0, operators_1.distinctUntilChanged)()
76
+ (0, operators_1.distinctUntilChanged)(
77
+ (previous, current) =>
78
+ previous.callId === current.callId && previous.state === current.state
79
+ ),
80
+ (0, operators_1.map)(({ call }) => call)
50
81
  );
51
82
  }
52
83
  /**
@@ -59,16 +90,7 @@ class CallStateController {
59
90
  * Current active call (synchronous access)
60
91
  */
61
92
  get currentActiveCall() {
62
- const calls = this.currentCalls;
63
- return (
64
- calls.find(
65
- (call) =>
66
- call.currentState === call_state_1.TelnyxCallState.RINGING ||
67
- call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
68
- call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
69
- call.currentState === call_state_1.TelnyxCallState.HELD
70
- ) || null
71
- );
93
+ return this._selectActiveCall(this.currentCalls);
72
94
  }
73
95
  /**
74
96
  * Access any active call tracked by the client.
@@ -94,6 +116,32 @@ class CallStateController {
94
116
  console.warn('CallStateController: Could not find call to set connecting:', callId);
95
117
  }
96
118
  }
119
+ /**
120
+ * Explicitly set the active call for multi-call scenarios.
121
+ * When set, activeCall$ and currentActiveCall prefer this call over
122
+ * the first-match heuristic. The ID is cleared automatically when the
123
+ * call reaches a terminal state.
124
+ * @param callId The ID of the call to mark as active
125
+ */
126
+ setActiveCall(callId) {
127
+ const call = this._callMap.get(callId);
128
+ if (call) {
129
+ this._activeCallId = callId;
130
+ this._calls.next([...this.currentCalls]);
131
+ } else {
132
+ console.warn('CallStateController: Cannot set active call - call not found:', callId);
133
+ }
134
+ }
135
+ /**
136
+ * Clear the explicitly-tracked active call ID, reverting to the
137
+ * first-match heuristic for active call selection.
138
+ */
139
+ clearActiveCall() {
140
+ if (this._activeCallId !== null) {
141
+ this._activeCallId = null;
142
+ this._calls.next([...this.currentCalls]);
143
+ }
144
+ }
97
145
  /**
98
146
  * Find a call by its underlying Telnyx call ID
99
147
  * @param telnyxCall The Telnyx call object to find
@@ -106,6 +154,31 @@ class CallStateController {
106
154
  }
107
155
  return null;
108
156
  }
157
+ /**
158
+ * Select the active call, preferring the explicitly-tracked call ID
159
+ * over the first-match heuristic.
160
+ */
161
+ _selectActiveCall(calls) {
162
+ if (this._activeCallId) {
163
+ const tracked = calls.find((c) => c.callId === this._activeCallId);
164
+ if (tracked && this._isNonTerminal(tracked)) {
165
+ return tracked;
166
+ }
167
+ }
168
+ // Fall back to first non-terminal call (backward compatible)
169
+ return calls.find((c) => this._isNonTerminal(c)) || null;
170
+ }
171
+ /**
172
+ * Check if a call is in a non-terminal (active or connecting) state.
173
+ */
174
+ _isNonTerminal(call) {
175
+ return (
176
+ call.currentState === call_state_1.TelnyxCallState.RINGING ||
177
+ call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
178
+ call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
179
+ call.currentState === call_state_1.TelnyxCallState.HELD
180
+ );
181
+ }
109
182
  /**
110
183
  * Initialize client listeners when the Telnyx client becomes available
111
184
  * This should be called by the session manager after client creation
@@ -133,7 +206,10 @@ class CallStateController {
133
206
  destinationNumber: destination,
134
207
  callerIdName: callerName,
135
208
  callerIdNumber: callerNumber,
136
- customHeaders,
209
+ customHeaders: this._normalizeCustomHeaders(customHeaders),
210
+ peerConnectionOptions: {
211
+ useTrickleIce: this._sessionManager.useTrickleIce,
212
+ },
137
213
  };
138
214
  const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
139
215
  // Create our wrapper Call object
@@ -154,6 +230,18 @@ class CallStateController {
154
230
  throw error;
155
231
  }
156
232
  }
233
+ /**
234
+ * Normalize public custom headers into the format expected by the underlying SDK.
235
+ */
236
+ _normalizeCustomHeaders(customHeaders) {
237
+ if (!customHeaders) {
238
+ return undefined;
239
+ }
240
+ if (Array.isArray(customHeaders)) {
241
+ return customHeaders;
242
+ }
243
+ return Object.entries(customHeaders).map(([name, value]) => ({ name, value }));
244
+ }
157
245
  /**
158
246
  * Set callbacks for waiting for invite logic (used for push notifications)
159
247
  */
@@ -182,6 +270,7 @@ class CallStateController {
182
270
  }
183
271
  }
184
272
  this._callMap.clear();
273
+ this._activeCallId = null;
185
274
  this._calls.next([]);
186
275
  }
187
276
  /**
@@ -192,9 +281,11 @@ class CallStateController {
192
281
  return;
193
282
  }
194
283
  this._disposed = true;
284
+ this._removeClientListeners();
195
285
  // Dispose of all calls
196
286
  this.currentCalls.forEach((call) => call.dispose());
197
287
  this._callMap.clear();
288
+ this._activeCallId = null;
198
289
  // CallKit cleanup is now handled by CallKitCoordinator automatically
199
290
  this._calls.complete();
200
291
  }
@@ -203,30 +294,22 @@ class CallStateController {
203
294
  */
204
295
  _setupClientListeners() {
205
296
  console.log('CallStateController: Setting up client listeners...');
206
- if (!this._sessionManager.telnyxClient) {
297
+ const telnyxClient = this._sessionManager.telnyxClient;
298
+ if (!telnyxClient) {
207
299
  console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
208
300
  return;
209
301
  }
302
+ this._removeClientListeners();
303
+ this._listenerClient = telnyxClient;
210
304
  console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
211
- console.log(
212
- 'CallStateController: Client instance:',
213
- this._sessionManager.telnyxClient.constructor.name
214
- );
305
+ console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
215
306
  // 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
- });
307
+ telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
220
308
  // 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
- });
309
+ telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
225
310
  // 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');
311
+ const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
312
+ const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
230
313
  console.log(
231
314
  'CallStateController: Listeners registered - incoming:',
232
315
  incomingListeners,
@@ -234,28 +317,21 @@ class CallStateController {
234
317
  reattachedListeners
235
318
  );
236
319
  // 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
- });
320
+ telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
251
321
  // 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
- });
322
+ telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
257
323
  console.log('CallStateController: Client listeners set up successfully');
258
324
  }
325
+ _removeClientListeners() {
326
+ if (!this._listenerClient) {
327
+ return;
328
+ }
329
+ this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
330
+ this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
331
+ this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
332
+ this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
333
+ this._listenerClient = undefined;
334
+ }
259
335
  /**
260
336
  * Handle incoming call or reattached call
261
337
  */
@@ -330,6 +406,10 @@ class CallStateController {
330
406
  */
331
407
  _addCall(call) {
332
408
  this._callMap.set(call.callId, call);
409
+ // Auto-track as active if no active call is currently set
410
+ if (this._activeCallId === null) {
411
+ this._activeCallId = call.callId;
412
+ }
333
413
  const currentCalls = this.currentCalls;
334
414
  currentCalls.push(call);
335
415
  this._calls.next([...currentCalls]);
@@ -342,7 +422,11 @@ class CallStateController {
342
422
  callkit_coordinator_1.callKitCoordinator.getCallKitUUID(telnyxCall);
343
423
  if (existingCallKitUUID) {
344
424
  console.log(
345
- 'CallStateController: Call already has CallKit integration, skipping duplicate report:',
425
+ 'CallStateController: Linking push-created CallKit call to its signaling call:',
426
+ existingCallKitUUID
427
+ );
428
+ callkit_coordinator_1.callKitCoordinator.linkExistingCallKitCall(
429
+ telnyxCall,
346
430
  existingCallKitUUID
347
431
  );
348
432
  } else if (call.isIncoming) {
@@ -367,16 +451,20 @@ class CallStateController {
367
451
  call.callState$.subscribe((state) => {
368
452
  // CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
369
453
  console.log('CallStateController: Call state changed to:', state);
454
+ // Re-emit the call list so calls$/activeCall$ subscribers see state changes.
455
+ // distinctUntilChanged uses reference equality; without a new array
456
+ // reference, non-terminal transitions leave activeCall$ stale.
457
+ this._calls.next([...this.currentCalls]);
370
458
  // Clean up when call ends - delay to next tick so external subscribers
371
459
  // receive the ENDED/FAILED state before the call is disposed
372
460
  if (
373
461
  state === call_state_1.TelnyxCallState.ENDED ||
374
462
  state === call_state_1.TelnyxCallState.FAILED
375
463
  ) {
376
- // Clear pending push data so the next app launch isn't mistaken for a push launch
377
- voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush().catch((e) =>
378
- console.warn('CallStateController: Failed to clear pending voip push:', e)
379
- );
464
+ // Clear active call ID if the ending call was the tracked active call
465
+ if (this._activeCallId === call.callId) {
466
+ this._activeCallId = null;
467
+ }
380
468
  setTimeout(() => this._removeCall(call.callId), 0);
381
469
  }
382
470
  });
@@ -14,6 +14,10 @@ export declare class SessionManager {
14
14
  private _currentConfig?;
15
15
  private _sessionId;
16
16
  private _disposed;
17
+ private _disposing;
18
+ private _connectionGeneration;
19
+ private _disposePromise?;
20
+ private _connectPromise?;
17
21
  private _onClientReady?;
18
22
  private _onDisconnect?;
19
23
  constructor();
@@ -42,6 +46,7 @@ export declare class SessionManager {
42
46
  * Get the underlying Telnyx client instance
43
47
  */
44
48
  get telnyxClient(): TelnyxSDK.TelnyxRTC | undefined;
49
+ get useTrickleIce(): boolean;
45
50
  /**
46
51
  * Connect using credential authentication
47
52
  */
@@ -75,14 +80,17 @@ export declare class SessionManager {
75
80
  * Handle push notification (async version)
76
81
  */
77
82
  handlePushNotification(payload: Record<string, any>): Promise<void>;
83
+ private _hasActiveOrHeldCall;
78
84
  /**
79
85
  * Dispose of the session manager and clean up resources
80
86
  */
81
- dispose(): void;
87
+ dispose(): Promise<void>;
82
88
  /**
83
89
  * Internal method to establish connection with or without push notification handling
84
90
  */
85
91
  private _connect;
92
+ private _dispose;
93
+ private _runConnect;
86
94
  /**
87
95
  * Set up event listeners for the Telnyx client
88
96
  */
@@ -95,4 +103,11 @@ export declare class SessionManager {
95
103
  * Generate a unique session ID
96
104
  */
97
105
  private _generateSessionId;
106
+ private _assertCanStartConnection;
107
+ private _isTeardownActive;
108
+ private _isConnectCanceled;
109
+ private _throwIfConnectCanceled;
110
+ private _createConnectCanceledError;
111
+ private _disconnectClient;
112
+ private _disconnectAndForgetClient;
98
113
  }