@telnyx/react-voice-commons-sdk 0.1.2 → 0.1.4

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 (55) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +469 -483
  3. package/ios/CallKitBridge.swift +2 -7
  4. package/lib/callkit/callkit-coordinator.d.ts +110 -117
  5. package/lib/callkit/callkit-coordinator.js +664 -727
  6. package/lib/callkit/callkit.d.ts +41 -41
  7. package/lib/callkit/callkit.js +252 -242
  8. package/lib/callkit/index.js +15 -47
  9. package/lib/callkit/use-callkit.d.ts +19 -19
  10. package/lib/callkit/use-callkit.js +270 -310
  11. package/lib/context/TelnyxVoiceContext.d.ts +9 -9
  12. package/lib/context/TelnyxVoiceContext.js +10 -13
  13. package/lib/hooks/use-callkit-coordinator.d.ts +9 -17
  14. package/lib/hooks/use-callkit-coordinator.js +45 -50
  15. package/lib/hooks/useAppReadyNotifier.js +13 -15
  16. package/lib/hooks/useAppStateHandler.d.ts +6 -11
  17. package/lib/hooks/useAppStateHandler.js +95 -110
  18. package/lib/hooks/useNetworkStateHandler.d.ts +0 -0
  19. package/lib/hooks/useNetworkStateHandler.js +0 -0
  20. package/lib/index.d.ts +3 -21
  21. package/lib/index.js +50 -201
  22. package/lib/internal/CallKitHandler.d.ts +6 -6
  23. package/lib/internal/CallKitHandler.js +96 -104
  24. package/lib/internal/callkit-manager.d.ts +57 -57
  25. package/lib/internal/callkit-manager.js +299 -316
  26. package/lib/internal/calls/call-state-controller.d.ts +73 -86
  27. package/lib/internal/calls/call-state-controller.js +263 -307
  28. package/lib/internal/session/session-manager.d.ts +71 -75
  29. package/lib/internal/session/session-manager.js +360 -424
  30. package/lib/internal/user-defaults-helpers.js +49 -39
  31. package/lib/internal/voice-pn-bridge.d.ts +114 -12
  32. package/lib/internal/voice-pn-bridge.js +212 -5
  33. package/lib/models/call-state.d.ts +46 -44
  34. package/lib/models/call-state.js +70 -68
  35. package/lib/models/call.d.ts +161 -133
  36. package/lib/models/call.js +454 -382
  37. package/lib/models/config.d.ts +11 -18
  38. package/lib/models/config.js +37 -35
  39. package/lib/models/connection-state.d.ts +10 -10
  40. package/lib/models/connection-state.js +16 -16
  41. package/lib/telnyx-voice-app.d.ts +28 -28
  42. package/lib/telnyx-voice-app.js +513 -480
  43. package/lib/telnyx-voip-client.d.ts +167 -167
  44. package/lib/telnyx-voip-client.js +385 -390
  45. package/package.json +115 -104
  46. package/src/callkit/callkit-coordinator.ts +830 -846
  47. package/src/hooks/useNetworkStateHandler.ts +0 -0
  48. package/src/internal/calls/call-state-controller.ts +407 -384
  49. package/src/internal/session/session-manager.ts +483 -467
  50. package/src/internal/voice-pn-bridge.ts +266 -18
  51. package/src/models/call-state.ts +105 -98
  52. package/src/models/call.ts +502 -388
  53. package/src/telnyx-voice-app.tsx +788 -690
  54. package/src/telnyx-voip-client.ts +551 -539
  55. package/src/types/telnyx-sdk.d.ts +93 -79
@@ -1,384 +1,407 @@
1
- import { BehaviorSubject, Observable } from 'rxjs';
2
- import { distinctUntilChanged, map } from 'rxjs/operators';
3
- import { TelnyxRTC, Call as TelnyxCall, type CallOptions } from '@telnyx/react-native-voice-sdk';
4
- import { Call } from '../../models/call';
5
- import { TelnyxCallState } from '../../models/call-state';
6
- import { SessionManager } from '../session/session-manager';
7
- import { callKitCoordinator } from '../../callkit/callkit-coordinator';
8
-
9
- /**
10
- * Central state machine for call management.
11
- *
12
- * This class manages all active calls, handles call state transitions,
13
- * and provides reactive streams for call-related state changes.
14
- */
15
- export class CallStateController {
16
- private readonly _calls = new BehaviorSubject<Call[]>([]);
17
- private readonly _callMap = new Map<string, Call>();
18
- private _disposed = false;
19
-
20
- // Callbacks for waiting for invite logic (used for push notifications)
21
- private _isWaitingForInvite?: () => boolean;
22
- private _onInviteAutoAccepted?: () => void;
23
-
24
- constructor(private readonly _sessionManager: SessionManager) {
25
- // Don't set up client listeners here - client doesn't exist yet
26
- // Will be called when client is available
27
- }
28
-
29
- /**
30
- * Observable stream of all current calls
31
- */
32
- get calls$(): Observable<Call[]> {
33
- return this._calls.asObservable().pipe(distinctUntilChanged());
34
- }
35
-
36
- /**
37
- * Observable stream of the currently active call
38
- */
39
- get activeCall$(): Observable<Call | null> {
40
- return this.calls$.pipe(
41
- map((calls) => {
42
- // Find the first call that is not terminated (includes RINGING, CONNECTING, ACTIVE, HELD)
43
- return (
44
- calls.find(
45
- (call) =>
46
- call.currentState === TelnyxCallState.RINGING ||
47
- call.currentState === TelnyxCallState.CONNECTING ||
48
- call.currentState === TelnyxCallState.ACTIVE ||
49
- call.currentState === TelnyxCallState.HELD
50
- ) || null
51
- );
52
- }),
53
- distinctUntilChanged()
54
- );
55
- }
56
-
57
- /**
58
- * Current list of calls (synchronous access)
59
- */
60
- get currentCalls(): Call[] {
61
- return this._calls.value;
62
- }
63
-
64
- /**
65
- * Current active call (synchronous access)
66
- */
67
- get currentActiveCall(): Call | null {
68
- const calls = this.currentCalls;
69
- return (
70
- calls.find(
71
- (call) =>
72
- call.currentState === TelnyxCallState.RINGING ||
73
- call.currentState === TelnyxCallState.CONNECTING ||
74
- call.currentState === TelnyxCallState.ACTIVE ||
75
- call.currentState === TelnyxCallState.HELD
76
- ) || null
77
- );
78
- }
79
-
80
- /**
81
- * Set a call to connecting state (used for push notification calls when answered via CallKit)
82
- * @param callId The ID of the call to set to connecting state
83
- */
84
- setCallConnecting(callId: string): void {
85
- const call = this._callMap.get(callId);
86
- if (call) {
87
- console.log('CallStateController: Setting call to connecting state:', callId);
88
- call.setConnecting();
89
- } else {
90
- console.warn('CallStateController: Could not find call to set connecting:', callId);
91
- }
92
- }
93
-
94
- /**
95
- * Find a call by its underlying Telnyx call ID
96
- * @param telnyxCall The Telnyx call object to find
97
- */
98
- findCallByTelnyxCall(telnyxCall: any): Call | null {
99
- for (const call of this._callMap.values()) {
100
- if (call.telnyxCall === telnyxCall || call.telnyxCall.callId === telnyxCall.callId) {
101
- return call;
102
- }
103
- }
104
- return null;
105
- }
106
-
107
- /**
108
- * Initialize client listeners when the Telnyx client becomes available
109
- * This should be called by the session manager after client creation
110
- */
111
- initializeClientListeners(): void {
112
- console.log('🔧 CallStateController: initializeClientListeners called');
113
- this._setupClientListeners();
114
-
115
- // CallKit integration now handled by CallKitCoordinator
116
- console.log('🔧 CallStateController: Using CallKitCoordinator for CallKit integration');
117
- }
118
-
119
- /**
120
- * Initiate a new outgoing call
121
- */
122
- async newCall(
123
- destination: string,
124
- callerName?: string,
125
- callerNumber?: string,
126
- debug: boolean = false
127
- ): Promise<Call> {
128
- if (this._disposed) {
129
- throw new Error('CallStateController has been disposed');
130
- }
131
-
132
- if (!this._sessionManager.telnyxClient) {
133
- throw new Error('Telnyx client not available');
134
- }
135
-
136
- try {
137
- // Create the call using the Telnyx SDK
138
- const callOptions: CallOptions = {
139
- destinationNumber: destination,
140
- callerIdName: callerName,
141
- callerIdNumber: callerNumber,
142
- };
143
- const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
144
-
145
- // Create our wrapper Call object
146
- const call = new Call(
147
- telnyxCall,
148
- telnyxCall.callId || this._generateCallId(),
149
- destination,
150
- false // outgoing call
151
- );
152
-
153
- // Add to our call tracking
154
- this._addCall(call);
155
-
156
- return call;
157
- } catch (error) {
158
- console.error('Failed to create new call:', error);
159
- throw error;
160
- }
161
- }
162
-
163
- /**
164
- * Set callbacks for waiting for invite logic (used for push notifications)
165
- */
166
- setWaitingForInviteCallbacks(callbacks: {
167
- isWaitingForInvite: () => boolean;
168
- onInviteAutoAccepted: () => void;
169
- }): void {
170
- this._isWaitingForInvite = callbacks.isWaitingForInvite;
171
- this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
172
- }
173
-
174
- /**
175
- * Dispose of the controller and clean up resources
176
- */
177
- dispose(): void {
178
- if (this._disposed) {
179
- return;
180
- }
181
-
182
- this._disposed = true;
183
-
184
- // Dispose of all calls
185
- this.currentCalls.forEach((call) => call.dispose());
186
- this._callMap.clear();
187
-
188
- // CallKit cleanup is now handled by CallKitCoordinator automatically
189
-
190
- this._calls.complete();
191
- }
192
-
193
- /**
194
- * Set up event listeners for the Telnyx client
195
- */
196
- private _setupClientListeners(): void {
197
- console.log('🔧 CallStateController: Setting up client listeners...');
198
-
199
- if (!this._sessionManager.telnyxClient) {
200
- console.log('🔧 CallStateController: No telnyxClient available yet, skipping listener setup');
201
- return;
202
- }
203
-
204
- console.log('🔧 CallStateController: TelnyxClient found, setting up incoming call listener');
205
-
206
- // Listen for incoming calls
207
- this._sessionManager.telnyxClient.on(
208
- 'telnyx.call.incoming',
209
- (telnyxCall: TelnyxCall, msg: any) => {
210
- console.log('📞 CallStateController: Incoming call received:', telnyxCall.callId);
211
- this._handleIncomingCall(telnyxCall, msg);
212
- }
213
- );
214
-
215
- // Listen for other call events if needed
216
- // this._sessionManager.telnyxClient.on('telnyx.call.stateChange', this._handleCallStateChange.bind(this));
217
-
218
- console.log('🔧 CallStateController: Client listeners set up successfully');
219
- }
220
-
221
- /**
222
- * Handle incoming call
223
- */
224
- private _handleIncomingCall(telnyxCall: TelnyxCall, inviteMsg?: any): void {
225
- const callId = telnyxCall.callId || this._generateCallId();
226
-
227
- console.log('📞 CallStateController: Handling incoming call:', callId);
228
- console.log('📞 CallStateController: TelnyxCall object:', telnyxCall);
229
- console.log('📞 CallStateController: Invite message:', inviteMsg);
230
-
231
- // Check if we already have this call
232
- if (this._callMap.has(callId)) {
233
- console.log('Call already exists:', callId);
234
- return;
235
- }
236
-
237
- // Get caller information from the invite message (preferred) or fallback to TelnyxCall
238
- let callerNumber = 'Unknown';
239
- let callerName = 'Unknown';
240
-
241
- if (inviteMsg && inviteMsg.params) {
242
- callerNumber = inviteMsg.params.caller_id_number || 'Unknown';
243
- callerName = inviteMsg.params.caller_id_name || callerNumber;
244
- console.log(
245
- '📞 CallStateController: Extracted caller info from invite - Number:',
246
- callerNumber,
247
- 'Name:',
248
- callerName
249
- );
250
- } else {
251
- // Fallback to TelnyxCall properties
252
- callerNumber = telnyxCall.remoteCallerIdNumber || 'Unknown';
253
- callerName = telnyxCall.remoteCallerIdName || callerNumber;
254
- console.log(
255
- '📞 CallStateController: Extracted caller info from TelnyxCall - Number:',
256
- callerNumber,
257
- 'Name:',
258
- callerName
259
- );
260
- }
261
-
262
- // Create our wrapper Call object
263
- const call = new Call(
264
- telnyxCall,
265
- callId,
266
- callerNumber, // Use caller number as destination for incoming calls
267
- true // incoming call
268
- );
269
-
270
- // Check if we're waiting for an invite (push notification scenario)
271
- if (this._isWaitingForInvite && this._isWaitingForInvite()) {
272
- console.log('Auto-accepting call from push notification');
273
- call.answer().catch((error) => {
274
- console.error('Failed to auto-accept call:', error);
275
- });
276
-
277
- if (this._onInviteAutoAccepted) {
278
- this._onInviteAutoAccepted();
279
- }
280
- }
281
-
282
- // Add to our call tracking - CallKit integration happens in _addCall
283
- this._addCall(call);
284
- }
285
-
286
- /**
287
- * Handle call state changes from the Telnyx client
288
- */
289
- private _handleCallStateChange(event: any): void {
290
- const callId = event.callId || event.id;
291
- const call = this._callMap.get(callId);
292
-
293
- if (call) {
294
- // The Call object will handle its own state updates through its listeners
295
- console.log(`Call ${callId} state changed to ${event.state}`);
296
- } else {
297
- console.warn(`Received state change for unknown call: ${callId}`);
298
- }
299
- }
300
-
301
- /**
302
- * Handle call updates from notifications
303
- */
304
- private _handleCallUpdate(callData: any): void {
305
- const callId = callData.id;
306
- const call = this._callMap.get(callId);
307
-
308
- if (call) {
309
- // Update call state based on the notification
310
- console.log(`Call ${callId} updated:`, callData);
311
- } else {
312
- console.warn(`Received update for unknown call: ${callId}`);
313
- }
314
- }
315
-
316
- /**
317
- * Add a call to our tracking
318
- */
319
- private _addCall(call: Call): void {
320
- this._callMap.set(call.callId, call);
321
-
322
- const currentCalls = this.currentCalls;
323
- currentCalls.push(call);
324
- this._calls.next([...currentCalls]);
325
-
326
- // Integrate with CallKit using CallKitCoordinator
327
- if (callKitCoordinator.isAvailable()) {
328
- // Get the underlying TelnyxCall for CallKitCoordinator
329
- const telnyxCall = call.telnyxCall;
330
-
331
- // Check if this call already has CallKit integration (e.g., from push notification)
332
- const existingCallKitUUID = callKitCoordinator.getCallKitUUID(telnyxCall);
333
-
334
- if (existingCallKitUUID) {
335
- console.log(
336
- 'CallStateController: Call already has CallKit integration, skipping duplicate report:',
337
- existingCallKitUUID
338
- );
339
- } else if (call.isIncoming) {
340
- // Handle incoming call with CallKit (only if not already integrated)
341
- console.log('CallStateController: Reporting incoming call to CallKitCoordinator');
342
- callKitCoordinator.reportIncomingCall(telnyxCall, call.destination, call.destination);
343
- } else {
344
- // Handle outgoing call with CallKit
345
- console.log('CallStateController: Starting outgoing call with CallKitCoordinator');
346
- callKitCoordinator.startOutgoingCall(telnyxCall, call.destination, call.destination);
347
- }
348
- }
349
-
350
- // Listen for call state changes - CallKitCoordinator handles this automatically
351
- call.callState$.subscribe((state) => {
352
- // CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
353
- console.log('CallStateController: Call state changed to:', state);
354
-
355
- // Clean up when call ends
356
- if (state === TelnyxCallState.ENDED || state === TelnyxCallState.FAILED) {
357
- this._removeCall(call.callId);
358
- }
359
- });
360
- }
361
-
362
- /**
363
- * Remove a call from our tracking
364
- */
365
- private _removeCall(callId: string): void {
366
- const call = this._callMap.get(callId);
367
- if (call) {
368
- // CallKit cleanup is handled automatically by CallKitCoordinator
369
-
370
- call.dispose();
371
- this._callMap.delete(callId);
372
-
373
- const currentCalls = this.currentCalls.filter((c) => c.callId !== callId);
374
- this._calls.next(currentCalls);
375
- }
376
- }
377
-
378
- /**
379
- * Generate a unique call ID
380
- */
381
- private _generateCallId(): string {
382
- return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
383
- }
384
- }
1
+ import { BehaviorSubject, Observable } from 'rxjs';
2
+ import { distinctUntilChanged, map } from 'rxjs/operators';
3
+ import { TelnyxRTC, Call as TelnyxCall, type CallOptions } from '@telnyx/react-native-voice-sdk';
4
+ import { Call } from '../../models/call';
5
+ import { TelnyxCallState } from '../../models/call-state';
6
+ import { SessionManager } from '../session/session-manager';
7
+ import { callKitCoordinator } from '../../callkit/callkit-coordinator';
8
+
9
+ /**
10
+ * Central state machine for call management.
11
+ *
12
+ * This class manages all active calls, handles call state transitions,
13
+ * and provides reactive streams for call-related state changes.
14
+ */
15
+ export class CallStateController {
16
+ private readonly _calls = new BehaviorSubject<Call[]>([]);
17
+ private readonly _callMap = new Map<string, Call>();
18
+ private _disposed = false;
19
+
20
+ // Callbacks for waiting for invite logic (used for push notifications)
21
+ private _isWaitingForInvite?: () => boolean;
22
+ private _onInviteAutoAccepted?: () => void;
23
+
24
+ constructor(private readonly _sessionManager: SessionManager) {
25
+ console.log('🔧 CallStateController: Constructor called - instance created');
26
+ // Don't set up client listeners here - client doesn't exist yet
27
+ // Will be called when client is available
28
+ }
29
+
30
+ /**
31
+ * Observable stream of all current calls
32
+ */
33
+ get calls$(): Observable<Call[]> {
34
+ return this._calls.asObservable().pipe(distinctUntilChanged());
35
+ }
36
+
37
+ /**
38
+ * Observable stream of the currently active call
39
+ */
40
+ get activeCall$(): Observable<Call | null> {
41
+ return this.calls$.pipe(
42
+ map((calls) => {
43
+ // Find the first call that is not terminated (includes RINGING, CONNECTING, ACTIVE, HELD)
44
+ return (
45
+ calls.find(
46
+ (call) =>
47
+ call.currentState === TelnyxCallState.RINGING ||
48
+ call.currentState === TelnyxCallState.CONNECTING ||
49
+ call.currentState === TelnyxCallState.ACTIVE ||
50
+ call.currentState === TelnyxCallState.HELD
51
+ ) || null
52
+ );
53
+ }),
54
+ distinctUntilChanged()
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Current list of calls (synchronous access)
60
+ */
61
+ get currentCalls(): Call[] {
62
+ return this._calls.value;
63
+ }
64
+
65
+ /**
66
+ * Current active call (synchronous access)
67
+ */
68
+ get currentActiveCall(): Call | null {
69
+ const calls = this.currentCalls;
70
+ return (
71
+ calls.find(
72
+ (call) =>
73
+ call.currentState === TelnyxCallState.RINGING ||
74
+ call.currentState === TelnyxCallState.CONNECTING ||
75
+ call.currentState === TelnyxCallState.ACTIVE ||
76
+ call.currentState === TelnyxCallState.HELD
77
+ ) || null
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Set a call to connecting state (used for push notification calls when answered via CallKit)
83
+ * @param callId The ID of the call to set to connecting state
84
+ */
85
+ setCallConnecting(callId: string): void {
86
+ const call = this._callMap.get(callId);
87
+ if (call) {
88
+ console.log('CallStateController: Setting call to connecting state:', callId);
89
+ call.setConnecting();
90
+ } else {
91
+ console.warn('CallStateController: Could not find call to set connecting:', callId);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Find a call by its underlying Telnyx call ID
97
+ * @param telnyxCall The Telnyx call object to find
98
+ */
99
+ findCallByTelnyxCall(telnyxCall: any): Call | null {
100
+ for (const call of this._callMap.values()) {
101
+ if (call.telnyxCall === telnyxCall || call.telnyxCall.callId === telnyxCall.callId) {
102
+ return call;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Initialize client listeners when the Telnyx client becomes available
110
+ * This should be called by the session manager after client creation
111
+ */
112
+ initializeClientListeners(): void {
113
+ console.log('🔧 CallStateController: initializeClientListeners called');
114
+ console.log(
115
+ '🔧 CallStateController: Current client exists:',
116
+ !!this._sessionManager.telnyxClient
117
+ );
118
+ this._setupClientListeners();
119
+
120
+ // CallKit integration now handled by CallKitCoordinator
121
+ console.log('🔧 CallStateController: Using CallKitCoordinator for CallKit integration');
122
+ }
123
+
124
+ /**
125
+ * Initiate a new outgoing call
126
+ */
127
+ async newCall(
128
+ destination: string,
129
+ callerName?: string,
130
+ callerNumber?: string,
131
+ customHeaders?: Record<string, string>
132
+ ): Promise<Call> {
133
+ if (this._disposed) {
134
+ throw new Error('CallStateController has been disposed');
135
+ }
136
+
137
+ if (!this._sessionManager.telnyxClient) {
138
+ throw new Error('Telnyx client not available');
139
+ }
140
+
141
+ try {
142
+ // Create the call using the Telnyx SDK
143
+ const callOptions: CallOptions = {
144
+ destinationNumber: destination,
145
+ callerIdName: callerName,
146
+ callerIdNumber: callerNumber,
147
+ customHeaders,
148
+ };
149
+ const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
150
+
151
+ // Create our wrapper Call object
152
+ const call = new Call(
153
+ telnyxCall,
154
+ telnyxCall.callId || this._generateCallId(),
155
+ destination,
156
+ false, // outgoing call
157
+ false, // not reattached
158
+ callerName || destination, // use destination as fallback for caller name
159
+ callerNumber // original caller number
160
+ );
161
+
162
+ // Add to our call tracking
163
+ this._addCall(call);
164
+
165
+ return call;
166
+ } catch (error) {
167
+ console.error('Failed to create new call:', error);
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Set callbacks for waiting for invite logic (used for push notifications)
174
+ */
175
+ setWaitingForInviteCallbacks(callbacks: {
176
+ isWaitingForInvite: () => boolean;
177
+ onInviteAutoAccepted: () => void;
178
+ }): void {
179
+ this._isWaitingForInvite = callbacks.isWaitingForInvite;
180
+ this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
181
+ }
182
+
183
+ /**
184
+ * Dispose of the controller and clean up resources
185
+ */
186
+ dispose(): void {
187
+ if (this._disposed) {
188
+ return;
189
+ }
190
+
191
+ this._disposed = true;
192
+
193
+ // Dispose of all calls
194
+ this.currentCalls.forEach((call) => call.dispose());
195
+ this._callMap.clear();
196
+
197
+ // CallKit cleanup is now handled by CallKitCoordinator automatically
198
+
199
+ this._calls.complete();
200
+ }
201
+
202
+ /**
203
+ * Set up event listeners for the Telnyx client
204
+ */
205
+ private _setupClientListeners(): void {
206
+ console.log('🔧 CallStateController: Setting up client listeners...');
207
+
208
+ if (!this._sessionManager.telnyxClient) {
209
+ console.log('🔧 CallStateController: No telnyxClient available yet, skipping listener setup');
210
+ return;
211
+ }
212
+
213
+ console.log('🔧 CallStateController: TelnyxClient found, setting up incoming call listener');
214
+ console.log(
215
+ '🔧 CallStateController: Client instance:',
216
+ this._sessionManager.telnyxClient.constructor.name
217
+ );
218
+
219
+ // Listen for incoming calls
220
+ this._sessionManager.telnyxClient.on(
221
+ 'telnyx.call.incoming',
222
+ (telnyxCall: TelnyxCall, msg: any) => {
223
+ console.log('📞 CallStateController: Incoming call received:', telnyxCall.callId);
224
+ this._handleIncomingCall(telnyxCall, msg, false);
225
+ }
226
+ );
227
+
228
+ // Listen for reattached calls (after network reconnection)
229
+ this._sessionManager.telnyxClient.on(
230
+ 'telnyx.call.reattached',
231
+ (telnyxCall: TelnyxCall, msg: any) => {
232
+ console.log('📞 CallStateController: Reattached call received:', telnyxCall.callId);
233
+ this._handleIncomingCall(telnyxCall, msg, true);
234
+ }
235
+ );
236
+
237
+ // Verify listeners are set up
238
+ const incomingListeners =
239
+ this._sessionManager.telnyxClient.listenerCount('telnyx.call.incoming');
240
+ const reattachedListeners =
241
+ this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
242
+ console.log(
243
+ '🔧 CallStateController: Listeners registered - incoming:',
244
+ incomingListeners,
245
+ 'reattached:',
246
+ reattachedListeners
247
+ );
248
+
249
+ // Listen for other call events if needed
250
+ // this._sessionManager.telnyxClient.on('telnyx.call.stateChange', this._handleCallStateChange.bind(this));
251
+
252
+ console.log('🔧 CallStateController: Client listeners set up successfully');
253
+ }
254
+
255
+ /**
256
+ * Handle incoming call or reattached call
257
+ */
258
+ private _handleIncomingCall(
259
+ telnyxCall: TelnyxCall,
260
+ inviteMsg?: any,
261
+ isReattached: boolean = false
262
+ ): void {
263
+ const callId = telnyxCall.callId || this._generateCallId();
264
+
265
+ console.log(
266
+ '📞 CallStateController: Handling incoming call:',
267
+ callId,
268
+ 'isReattached:',
269
+ isReattached
270
+ );
271
+ console.log('📞 CallStateController: TelnyxCall object:', telnyxCall);
272
+ console.log('📞 CallStateController: Invite message:', inviteMsg);
273
+
274
+ // For reattached calls, remove existing call and create new one
275
+ if (isReattached && this._callMap.has(callId)) {
276
+ console.log('📞 CallStateController: Removing existing call for reattachment');
277
+ const existingCall = this._callMap.get(callId);
278
+ if (existingCall) {
279
+ console.log(
280
+ '📞 CallStateController: Existing call state before removal:',
281
+ existingCall.currentState
282
+ );
283
+ this._removeCall(callId);
284
+ }
285
+ }
286
+
287
+ // Check if we already have this call (for non-reattached calls)
288
+ if (this._callMap.has(callId) && !isReattached) {
289
+ console.log('Call already exists:', callId);
290
+ return;
291
+ }
292
+
293
+ // Get caller information from the invite message (preferred) or fallback to TelnyxCall
294
+ let callerNumber = '';
295
+ let callerName = '';
296
+
297
+ if (inviteMsg && inviteMsg.params) {
298
+ callerNumber = inviteMsg.params.caller_id_number || '';
299
+ callerName = inviteMsg.params.caller_id_name || '';
300
+ console.log(
301
+ '📞 CallStateController: Extracted caller info from invite - Number:',
302
+ callerNumber,
303
+ 'Name:',
304
+ callerName
305
+ );
306
+ } else {
307
+ // Fallback to TelnyxCall properties
308
+ callerNumber = telnyxCall.remoteCallerIdNumber || '';
309
+ callerName = telnyxCall.remoteCallerIdName || '';
310
+ console.log(
311
+ '📞 CallStateController: Extracted caller info from TelnyxCall - Number:',
312
+ callerNumber,
313
+ 'Name:',
314
+ callerName
315
+ );
316
+ }
317
+
318
+ // Use smart fallbacks - prefer caller number over "Unknown"
319
+ const finalCallerNumber = callerNumber || 'Unknown Number';
320
+ const finalCallerName = callerName || callerNumber || 'Unknown Caller';
321
+
322
+ // Create our wrapper Call object
323
+ const call = new Call(
324
+ telnyxCall,
325
+ callId,
326
+ finalCallerNumber, // Use caller number as destination for incoming calls
327
+ true, // incoming call
328
+ isReattached, // pass the reattached flag
329
+ finalCallerName, // use caller name or fallback to number
330
+ finalCallerNumber // use caller number
331
+ );
332
+
333
+ // Add to our call tracking - CallKit integration happens in _addCall
334
+ this._addCall(call);
335
+ }
336
+
337
+ /**
338
+ * Add a call to our tracking
339
+ */
340
+ private _addCall(call: Call): void {
341
+ this._callMap.set(call.callId, call);
342
+
343
+ const currentCalls = this.currentCalls;
344
+ currentCalls.push(call);
345
+ this._calls.next([...currentCalls]);
346
+
347
+ // Integrate with CallKit using CallKitCoordinator
348
+ if (callKitCoordinator.isAvailable()) {
349
+ // Get the underlying TelnyxCall for CallKitCoordinator
350
+ const telnyxCall = call.telnyxCall;
351
+
352
+ // Check if this call already has CallKit integration (e.g., from push notification)
353
+ const existingCallKitUUID = callKitCoordinator.getCallKitUUID(telnyxCall);
354
+
355
+ if (existingCallKitUUID) {
356
+ console.log(
357
+ 'CallStateController: Call already has CallKit integration, skipping duplicate report:',
358
+ existingCallKitUUID
359
+ );
360
+ } else if (call.isIncoming) {
361
+ // Handle incoming call with CallKit (only if not already integrated)
362
+ console.log('CallStateController: Reporting incoming call to CallKitCoordinator');
363
+ callKitCoordinator.reportIncomingCall(telnyxCall, call.destination, call.destination);
364
+ } else {
365
+ // Handle outgoing call with CallKit
366
+ console.log('CallStateController: Starting outgoing call with CallKitCoordinator');
367
+ callKitCoordinator.startOutgoingCall(telnyxCall, call.destination, call.destination);
368
+ }
369
+ }
370
+
371
+ // Listen for call state changes - CallKitCoordinator handles this automatically
372
+ call.callState$.subscribe((state) => {
373
+ // CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
374
+ console.log('CallStateController: Call state changed to:', state);
375
+
376
+ // Clean up when call ends
377
+ if (state === TelnyxCallState.ENDED || state === TelnyxCallState.FAILED) {
378
+ this._removeCall(call.callId);
379
+ }
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Remove a call from our tracking
385
+ */
386
+ private _removeCall(callId: string): void {
387
+ const call = this._callMap.get(callId);
388
+ if (call) {
389
+ console.log('CallStateController: Removing call:', callId);
390
+
391
+ // CallKit cleanup is handled automatically by CallKitCoordinator
392
+
393
+ call.dispose();
394
+ this._callMap.delete(callId);
395
+
396
+ const currentCalls = this.currentCalls.filter((c) => c.callId !== callId);
397
+ this._calls.next(currentCalls);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Generate a unique call ID
403
+ */
404
+ private _generateCallId(): string {
405
+ return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
406
+ }
407
+ }