@telnyx/react-voice-commons-sdk 0.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 (73) hide show
  1. package/TelnyxVoiceCommons.podspec +32 -0
  2. package/ios/CallKitBridge.m +44 -0
  3. package/ios/CallKitBridge.swift +879 -0
  4. package/ios/README.md +211 -0
  5. package/ios/VoicePnBridge.m +31 -0
  6. package/ios/VoicePnBridge.swift +87 -0
  7. package/lib/callkit/callkit-coordinator.d.ts +126 -0
  8. package/lib/callkit/callkit-coordinator.js +728 -0
  9. package/lib/callkit/callkit.d.ts +49 -0
  10. package/lib/callkit/callkit.js +262 -0
  11. package/lib/callkit/index.d.ts +4 -0
  12. package/lib/callkit/index.js +15 -0
  13. package/lib/callkit/use-callkit-coordinator.d.ts +21 -0
  14. package/lib/callkit/use-callkit-coordinator.js +53 -0
  15. package/lib/callkit/use-callkit.d.ts +28 -0
  16. package/lib/callkit/use-callkit.js +279 -0
  17. package/lib/context/TelnyxVoiceContext.d.ts +18 -0
  18. package/lib/context/TelnyxVoiceContext.js +18 -0
  19. package/lib/hooks/use-callkit-coordinator.d.ts +13 -0
  20. package/lib/hooks/use-callkit-coordinator.js +48 -0
  21. package/lib/hooks/useAppReadyNotifier.d.ts +9 -0
  22. package/lib/hooks/useAppReadyNotifier.js +25 -0
  23. package/lib/hooks/useAppStateHandler.d.ts +16 -0
  24. package/lib/hooks/useAppStateHandler.js +105 -0
  25. package/lib/index.d.ts +24 -0
  26. package/lib/index.js +66 -0
  27. package/lib/internal/CallKitHandler.d.ts +17 -0
  28. package/lib/internal/CallKitHandler.js +110 -0
  29. package/lib/internal/callkit-manager.d.ts +69 -0
  30. package/lib/internal/callkit-manager.js +326 -0
  31. package/lib/internal/calls/call-state-controller.d.ts +92 -0
  32. package/lib/internal/calls/call-state-controller.js +294 -0
  33. package/lib/internal/session/session-manager.d.ts +87 -0
  34. package/lib/internal/session/session-manager.js +385 -0
  35. package/lib/internal/user-defaults-helpers.d.ts +10 -0
  36. package/lib/internal/user-defaults-helpers.js +69 -0
  37. package/lib/internal/voice-pn-bridge.d.ts +14 -0
  38. package/lib/internal/voice-pn-bridge.js +5 -0
  39. package/lib/models/call-state.d.ts +61 -0
  40. package/lib/models/call-state.js +87 -0
  41. package/lib/models/call.d.ts +145 -0
  42. package/lib/models/call.js +372 -0
  43. package/lib/models/config.d.ts +64 -0
  44. package/lib/models/config.js +92 -0
  45. package/lib/models/connection-state.d.ts +34 -0
  46. package/lib/models/connection-state.js +50 -0
  47. package/lib/telnyx-voice-app.d.ts +48 -0
  48. package/lib/telnyx-voice-app.js +486 -0
  49. package/lib/telnyx-voip-client.d.ts +184 -0
  50. package/lib/telnyx-voip-client.js +386 -0
  51. package/package.json +104 -0
  52. package/src/callkit/callkit-coordinator.ts +846 -0
  53. package/src/callkit/callkit.ts +322 -0
  54. package/src/callkit/index.ts +4 -0
  55. package/src/callkit/use-callkit.ts +345 -0
  56. package/src/context/TelnyxVoiceContext.tsx +33 -0
  57. package/src/hooks/use-callkit-coordinator.ts +60 -0
  58. package/src/hooks/useAppReadyNotifier.ts +25 -0
  59. package/src/hooks/useAppStateHandler.ts +134 -0
  60. package/src/index.ts +56 -0
  61. package/src/internal/CallKitHandler.tsx +149 -0
  62. package/src/internal/callkit-manager.ts +335 -0
  63. package/src/internal/calls/call-state-controller.ts +384 -0
  64. package/src/internal/session/session-manager.ts +467 -0
  65. package/src/internal/user-defaults-helpers.ts +58 -0
  66. package/src/internal/voice-pn-bridge.ts +18 -0
  67. package/src/models/call-state.ts +98 -0
  68. package/src/models/call.ts +388 -0
  69. package/src/models/config.ts +125 -0
  70. package/src/models/connection-state.ts +50 -0
  71. package/src/telnyx-voice-app.tsx +690 -0
  72. package/src/telnyx-voip-client.ts +475 -0
  73. package/src/types/telnyx-sdk.d.ts +79 -0
@@ -0,0 +1,728 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.callKitCoordinator = void 0;
40
+ const react_native_1 = require("react-native");
41
+ const callkit_1 = __importStar(require("./callkit"));
42
+ const voice_pn_bridge_1 = require("../internal/voice-pn-bridge");
43
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
44
+ /**
45
+ * CallKit Coordinator - Manages the proper CallKit-first flow for iOS
46
+ *
47
+ * This coordinator ensures that all call actions go through CallKit first,
48
+ * which then triggers the appropriate WebRTC actions. This follows Apple's
49
+ * guidelines for proper CallKit integration.
50
+ */
51
+ class CallKitCoordinator {
52
+ static getInstance() {
53
+ if (!CallKitCoordinator.instance) {
54
+ CallKitCoordinator.instance = new CallKitCoordinator();
55
+ }
56
+ return CallKitCoordinator.instance;
57
+ }
58
+ constructor() {
59
+ // Maps CallKit UUIDs to WebRTC calls
60
+ this.callMap = new Map();
61
+ // Tracks calls that are being processed to prevent duplicates
62
+ this.processingCalls = new Set();
63
+ // Tracks calls that have already been ended in CallKit to prevent duplicate reports
64
+ this.endedCalls = new Set();
65
+ // Tracks calls that have already been reported as connected to prevent duplicate reports
66
+ this.connectedCalls = new Set();
67
+ this.isCallFromPush = false;
68
+ // Reference to the VoIP client for triggering reconnection when needed
69
+ this.voipClient = null;
70
+ if (react_native_1.Platform.OS === 'ios' && callkit_1.default.isAvailable()) {
71
+ this.setupCallKitListeners();
72
+ }
73
+ }
74
+ setupCallKitListeners() {
75
+ // Handle CallKit answer actions
76
+ callkit_1.default.onAnswerCall((event) => {
77
+ this.handleCallKitAnswer(event.callUUID, event);
78
+ });
79
+ // Handle CallKit end actions
80
+ callkit_1.default.onEndCall((event) => {
81
+ this.handleCallKitEnd(event.callUUID, event);
82
+ });
83
+ // Handle CallKit start actions (for outgoing calls)
84
+ callkit_1.default.onStartCall((event) => {
85
+ this.handleCallKitStart(event.callUUID);
86
+ });
87
+ // Handle CallKit push received events
88
+ callkit_1.default.onReceivePush((event) => {
89
+ this.handleCallKitPushReceived(event.callUUID, event);
90
+ });
91
+ }
92
+ /**
93
+ * Report an incoming call to CallKit (from push notification or socket)
94
+ * For push notifications, the call is already reported - we just need to map it
95
+ */
96
+ async reportIncomingCall(call, callerName, callerNumber) {
97
+ if (react_native_1.Platform.OS !== 'ios' || !callkit_1.default.isAvailable()) {
98
+ return null;
99
+ }
100
+ // This is a new call - report it to CallKit using the WebRTC call ID as CallKit UUID
101
+ const callKitUUID = call.callId;
102
+ console.log('CallKitCoordinator: Report Called called', {
103
+ callKitUUID,
104
+ webrtcCallId: call.callId,
105
+ callerName,
106
+ callerNumber,
107
+ isCallFromPush: this.isCallFromPush,
108
+ });
109
+ this.setupWebRTCCallListeners(call, callKitUUID);
110
+ this.callMap.set(callKitUUID, call);
111
+ try {
112
+ if (!this.isCallFromPush) {
113
+ console.log('CallKitCoordinator: Reporting new incoming call to CallKit', {
114
+ callKitUUID,
115
+ webrtcCallId: call.callId,
116
+ callerName,
117
+ callerNumber,
118
+ isCallFromPush: this.isCallFromPush,
119
+ });
120
+ const success = await callkit_1.default.reportIncomingCall(callKitUUID, callerNumber, callerName);
121
+ if (success) {
122
+ return callKitUUID;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ catch (error) {
128
+ console.error('CallKitCoordinator: Failed to report incoming call', error);
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Start an outgoing call through CallKit
134
+ */
135
+ async startOutgoingCall(call, destinationNumber, displayName) {
136
+ if (react_native_1.Platform.OS !== 'ios' || !callkit_1.default.isAvailable()) {
137
+ return null;
138
+ }
139
+ const callKitUUID = call.callId;
140
+ console.log('CallKitCoordinator: Starting outgoing call through CallKit', {
141
+ callKitUUID,
142
+ webrtcCallId: call.callId,
143
+ destinationNumber,
144
+ displayName,
145
+ });
146
+ try {
147
+ const success = await callkit_1.default.startOutgoingCall(callKitUUID, destinationNumber, displayName || destinationNumber);
148
+ if (success) {
149
+ this.callMap.set(callKitUUID, call);
150
+ this.setupWebRTCCallListeners(call, callKitUUID);
151
+ call._callKitUUID = callKitUUID;
152
+ return callKitUUID;
153
+ }
154
+ return null;
155
+ }
156
+ catch (error) {
157
+ console.error('CallKitCoordinator: Failed to start outgoing call', error);
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Answer a call from the app UI (CallKit-first approach)
163
+ */
164
+ async answerCallFromUI(call) {
165
+ // Use comprehensive UUID lookup that checks both maps and call properties
166
+ const callKitUUID = this.getCallKitUUID(call);
167
+ console.log('CallKitCoordinator: Answering call from UI using CallKit answer simulation', callKitUUID);
168
+ // Mark as processing to prevent duplicate actions
169
+ this.processingCalls.add(callKitUUID);
170
+ try {
171
+ // Simulate the CallKit answer action, which will trigger our answer handler
172
+ const success = await callkit_1.default.answerCall(callKitUUID);
173
+ if (success) {
174
+ // If CallKit answer fails, fallback to direct WebRTC answer
175
+ if (this.isCallFromPush) {
176
+ call.answer();
177
+ this.isCallFromPush = false;
178
+ }
179
+ console.log('CallKitCoordinator: CallKit answer success');
180
+ }
181
+ return success;
182
+ }
183
+ catch (error) {
184
+ console.error('CallKitCoordinator: Error answering call from UI', error);
185
+ return false;
186
+ }
187
+ finally {
188
+ this.processingCalls.delete(callKitUUID);
189
+ }
190
+ }
191
+ /**
192
+ * End a call from the app UI (CallKit-first approach)
193
+ */
194
+ async endCallFromUI(call) {
195
+ // Use comprehensive UUID lookup that checks both maps and call properties
196
+ const callKitUUID = this.getCallKitUUID(call);
197
+ if (!callKitUUID) {
198
+ console.warn('CallKitCoordinator: Cannot end call - no CallKit UUID found');
199
+ // Fallback to direct WebRTC hangup
200
+ call.hangup();
201
+ return false;
202
+ }
203
+ console.log('CallKitCoordinator: Ending call from UI - dismissing CallKit and hanging up WebRTC call', callKitUUID);
204
+ // Mark as processing to prevent duplicate actions
205
+ this.processingCalls.add(callKitUUID);
206
+ try {
207
+ // End the call in CallKit and hang up the WebRTC call
208
+ await callkit_1.default.endCall(callKitUUID);
209
+ call.hangup();
210
+ // Clean up the mappings
211
+ this.cleanupCall(callKitUUID);
212
+ return true;
213
+ }
214
+ catch (error) {
215
+ console.error('CallKitCoordinator: Error ending call from UI', error);
216
+ call.hangup(); // Ensure WebRTC call is ended
217
+ return false;
218
+ }
219
+ finally {
220
+ this.processingCalls.delete(callKitUUID);
221
+ }
222
+ }
223
+ /**
224
+ * Handle CallKit answer action (triggered by CallKit)
225
+ */
226
+ async handleCallKitAnswer(callKitUUID, event) {
227
+ if (this.processingCalls.has(callKitUUID)) {
228
+ console.log('CallKitCoordinator: Answer action already being processed, skipping duplicate');
229
+ return;
230
+ }
231
+ const call = this.callMap.get(callKitUUID);
232
+ if (!call) {
233
+ console.warn('CallKitCoordinator: No WebRTC call found for CallKit answer action', {
234
+ callKitUUID,
235
+ availableCallKitUUIDs: Array.from(this.callMap.keys()),
236
+ availableWebRTCCallIds: Array.from(this.callMap.values()).map((c) => c.callId),
237
+ });
238
+ console.log('CallKitCoordinator: No WebRTC call found, handling as push notification');
239
+ await this.handlePushNotificationAnswer(callKitUUID, event);
240
+ return;
241
+ }
242
+ console.log('CallKitCoordinator: Processing CallKit answer action', {
243
+ callKitUUID,
244
+ webrtcCallId: call.callId,
245
+ direction: call.direction,
246
+ currentState: call.state,
247
+ });
248
+ if (call.state === 'active' || call.state === 'connecting') {
249
+ console.log('CallKitCoordinator: Call already active/connecting, skipping duplicate answer action');
250
+ return;
251
+ }
252
+ this.processingCalls.add(callKitUUID);
253
+ try {
254
+ if (call.direction === 'inbound') {
255
+ const voipClient = this.getSDKClient();
256
+ if (voipClient) {
257
+ console.log('CallKitCoordinator: Setting incoming call to CONNECTING state for CallKit answer');
258
+ voipClient.setCallConnecting(call.callId);
259
+ }
260
+ // Report call as connected to CallKit to trigger audio session activation
261
+ setTimeout(async () => {
262
+ try {
263
+ await callkit_1.default.reportCallConnected(callKitUUID);
264
+ console.log('CallKitCoordinator: Reported call connected to activate audio session');
265
+ this.connectedCalls.add(callKitUUID);
266
+ }
267
+ catch (error) {
268
+ console.error('CallKitCoordinator: Error reporting call connected for audio session:', error);
269
+ }
270
+ }, 200);
271
+ setTimeout(() => {
272
+ call.answer();
273
+ }, 500);
274
+ }
275
+ else {
276
+ console.log('CallKitCoordinator: Outgoing call, skipping answer and CONNECTING state');
277
+ }
278
+ }
279
+ catch (error) {
280
+ console.error('CallKitCoordinator: Error processing CallKit answer', error);
281
+ await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
282
+ this.cleanupCall(callKitUUID);
283
+ }
284
+ finally {
285
+ this.processingCalls.delete(callKitUUID);
286
+ }
287
+ }
288
+ /**
289
+ * Handle CallKit end action (triggered by CallKit)
290
+ */
291
+ async handleCallKitEnd(callKitUUID, event) {
292
+ this.isCallFromPush = false;
293
+ if (this.processingCalls.has(callKitUUID)) {
294
+ console.log('CallKitCoordinator: End action already being processed, skipping duplicate');
295
+ return;
296
+ }
297
+ const call = this.callMap.get(callKitUUID);
298
+ if (!call) {
299
+ console.warn('CallKitCoordinator: No WebRTC call found for CallKit end action', {
300
+ callKitUUID,
301
+ availableCallKitUUIDs: Array.from(this.callMap.keys()),
302
+ availableWebRTCCallIds: Array.from(this.callMap.values()).map((c) => c.callId),
303
+ });
304
+ console.log('CallKitCoordinator: No WebRTC call found, handling as push notification rejection');
305
+ await this.handlePushNotificationReject(callKitUUID, event);
306
+ this.cleanupCall(callKitUUID);
307
+ return;
308
+ }
309
+ console.log('CallKitCoordinator: Processing CallKit end action', {
310
+ callKitUUID,
311
+ webrtcCallId: call.callId,
312
+ });
313
+ this.processingCalls.add(callKitUUID);
314
+ try {
315
+ call.hangup();
316
+ }
317
+ catch (error) {
318
+ console.error('CallKitCoordinator: Error hanging up WebRTC call', error);
319
+ }
320
+ finally {
321
+ this.processingCalls.delete(callKitUUID);
322
+ this.cleanupCall(callKitUUID);
323
+ // Check if app is in background and no more calls - disconnect client
324
+ await this.checkBackgroundDisconnection();
325
+ }
326
+ }
327
+ /**
328
+ * Handle CallKit start action (triggered by CallKit for outgoing calls)
329
+ */
330
+ async handleCallKitStart(callKitUUID) {
331
+ const call = this.callMap.get(callKitUUID);
332
+ if (!call) {
333
+ console.warn('CallKitCoordinator: No WebRTC call found for CallKit start action', callKitUUID);
334
+ return;
335
+ }
336
+ console.log('CallKitCoordinator: Processing CallKit start action', {
337
+ callKitUUID,
338
+ webrtcCallId: call.callId,
339
+ });
340
+ // For outgoing calls, the WebRTC call should already be initiated
341
+ // We just need to report when it connects
342
+ }
343
+ /**
344
+ * Handle CallKit push received event - when a VoIP push notification has been processed
345
+ * This allows us to coordinate between the push notification and any subsequent WebRTC calls
346
+ */
347
+ async handleCallKitPushReceived(callKitUUID, event) {
348
+ if (this.isCallFromPush) {
349
+ this.isCallFromPush = false;
350
+ console.log('CallKitCoordinator: Ignoring push received event (already processed)');
351
+ return;
352
+ }
353
+ console.log('CallKitCoordinator: Processing push received event', {
354
+ callKitUUID,
355
+ source: event?.callData?.source,
356
+ });
357
+ this.isCallFromPush = true;
358
+ console.log('CallKitCoordinator: Processing push received event', {
359
+ callKitUUID,
360
+ source: event?.callData?.source,
361
+ isCallFromPush: this.isCallFromPush,
362
+ });
363
+ try {
364
+ // Get VoIP client instance
365
+ const voipClient = this.getSDKClient();
366
+ if (!voipClient) {
367
+ console.error('CallKitCoordinator: VoIP client not available');
368
+ return;
369
+ }
370
+ // Retrieve pending push data from VoIP bridge
371
+ const pendingPushJson = await voice_pn_bridge_1.VoicePnBridge.getPendingVoipPush();
372
+ if (!pendingPushJson) {
373
+ console.warn('CallKitCoordinator: No pending push data found');
374
+ return;
375
+ }
376
+ const pendingPush = JSON.parse(pendingPushJson);
377
+ const realPushData = pendingPush?.payload;
378
+ if (!realPushData?.metadata) {
379
+ console.warn('CallKitCoordinator: Invalid push data structure');
380
+ return;
381
+ }
382
+ // Prepare push metadata with CallKit flag
383
+ const enhancedMetadata = {
384
+ ...realPushData.metadata,
385
+ from_callkit: true,
386
+ };
387
+ // Check if auto-answer is set and add from_notification flag
388
+ const autoAnswerFlag = await async_storage_1.default.getItem('@auto_answer_next_call');
389
+ const shouldAddFromNotification = autoAnswerFlag === 'true';
390
+ let pushData;
391
+ if (shouldAddFromNotification) {
392
+ pushData = {
393
+ metadata: enhancedMetadata,
394
+ from_notification: true,
395
+ };
396
+ }
397
+ else {
398
+ pushData = {
399
+ metadata: enhancedMetadata,
400
+ };
401
+ }
402
+ // Process the push notification
403
+ await voipClient.handlePushNotification(pushData);
404
+ console.log('CallKitCoordinator: Push notification processed successfully');
405
+ }
406
+ catch (error) {
407
+ console.error('CallKitCoordinator: Error processing push received event:', error);
408
+ }
409
+ }
410
+ /**
411
+ * Handle push notification answer - when user answers from CallKit but we don't have a WebRTC call yet
412
+ * This is the iOS equivalent of the Android FCM handler
413
+ */
414
+ async handlePushNotificationAnswer(callKitUUID, event) {
415
+ try {
416
+ console.log('CallKitCoordinator: Handling push notification answer for CallKit UUID:', callKitUUID);
417
+ if (react_native_1.Platform.OS === 'ios') {
418
+ console.log('CallKitCoordinator: Processing iOS push notification answer');
419
+ // Set auto-answer flag so when the WebRTC call comes in, it will be answered automatically
420
+ await async_storage_1.default.setItem('@auto_answer_next_call', 'true');
421
+ console.log('CallKitCoordinator: ✅ Set auto-answer flag for next incoming call');
422
+ // Store the CallKit UUID so we can link it when the WebRTC call arrives
423
+ await async_storage_1.default.setItem('@pending_callkit_uuid', callKitUUID);
424
+ console.log('CallKitCoordinator: ✅ Stored pending CallKit UUID for linking');
425
+ // Get VoIP client and trigger reconnection
426
+ const voipClient = this.getSDKClient();
427
+ if (!voipClient) {
428
+ console.error('CallKitCoordinator: ❌ No VoIP client available - cannot reconnect for push notification');
429
+ await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
430
+ this.cleanupCall(callKitUUID);
431
+ return;
432
+ }
433
+ // Get the real push data that was stored by the VoIP push handler
434
+ console.log('CallKitCoordinator: 🔍 Getting real push data from VoicePnBridge...');
435
+ let realPushData = null;
436
+ try {
437
+ const pendingPushJson = await voice_pn_bridge_1.VoicePnBridge.getPendingVoipPush();
438
+ if (pendingPushJson) {
439
+ const pendingPush = JSON.parse(pendingPushJson);
440
+ if (pendingPush && pendingPush.payload) {
441
+ console.log('CallKitCoordinator: ✅ Found real push data');
442
+ realPushData = pendingPush.payload;
443
+ }
444
+ }
445
+ }
446
+ catch (error) {
447
+ console.warn('CallKitCoordinator: Could not get real push data:', error);
448
+ }
449
+ // Create push notification payload - use real data if available, fallback to placeholder
450
+ const pushAction = 'incoming_call';
451
+ let pushMetadata;
452
+ if (realPushData && realPushData.metadata) {
453
+ // Use the real push metadata
454
+ console.log('CallKitCoordinator: 🎯 Using REAL push metadata for immediate handling');
455
+ pushMetadata = JSON.stringify({
456
+ ...realPushData.metadata,
457
+ from_callkit: true, // Add flag to indicate this was answered via CallKit
458
+ });
459
+ }
460
+ else {
461
+ // Fallback to placeholder (this should rarely happen)
462
+ console.warn('CallKitCoordinator: ⚠️ No real push data found, using placeholder');
463
+ pushMetadata = JSON.stringify({
464
+ call_id: callKitUUID,
465
+ caller_name: 'Incoming Call',
466
+ caller_number: 'Unknown',
467
+ voice_sdk_id: 'unknown',
468
+ sent_time: new Date().toISOString(),
469
+ from_callkit: true,
470
+ });
471
+ }
472
+ // Set the pending push action using VoicePnBridge
473
+ await voice_pn_bridge_1.VoicePnBridge.setPendingPushAction(pushAction, pushMetadata);
474
+ console.log('CallKitCoordinator: ✅ Set pending push action for reconnection');
475
+ return;
476
+ }
477
+ // For other platforms (shouldn't happen on iOS)
478
+ console.error('CallKitCoordinator: ❌ Unsupported platform for push notification handling');
479
+ await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
480
+ }
481
+ catch (error) {
482
+ console.error('CallKitCoordinator: ❌ Error handling push notification answer:', error);
483
+ // Report the call as failed to CallKit
484
+ await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
485
+ this.cleanupCall(callKitUUID);
486
+ }
487
+ }
488
+ /**
489
+ * Handle push notification reject - when user rejects from CallKit but we don't have a WebRTC call yet
490
+ * This is the iOS equivalent of the Android FCM handler reject
491
+ */
492
+ async handlePushNotificationReject(callKitUUID, event) {
493
+ try {
494
+ console.log('CallKitCoordinator: Handling push notification rejection for CallKit UUID:', callKitUUID);
495
+ if (react_native_1.Platform.OS === 'ios') {
496
+ console.log('CallKitCoordinator: Processing iOS push notification rejection');
497
+ this.voipClient.queueEndFromCallKit();
498
+ // Clean up push notification state
499
+ await this.cleanupPushNotificationState();
500
+ console.log('CallKitCoordinator: 🎯 Push notification rejection handling complete');
501
+ return;
502
+ }
503
+ // For other platforms (shouldn't happen on iOS)
504
+ console.error('CallKitCoordinator: ❌ Unsupported platform for push notification rejection handling');
505
+ }
506
+ catch (error) {
507
+ console.error('CallKitCoordinator: ❌ Error handling push notification rejection:', error);
508
+ }
509
+ }
510
+ /**
511
+ * Set up listeners for WebRTC call state changes
512
+ */
513
+ setupWebRTCCallListeners(call, callKitUUID) {
514
+ const handleStateChange = async (call, state) => {
515
+ console.log('CallKitCoordinator: WebRTC call state changed', {
516
+ callKitUUID,
517
+ webrtcCallId: call.callId,
518
+ state,
519
+ });
520
+ switch (state) {
521
+ case 'active':
522
+ // When WebRTC call becomes active, just report as connected
523
+ // (CallKit call was already answered in answerCallFromUI)
524
+ if (!this.connectedCalls.has(callKitUUID)) {
525
+ console.log('CallKitCoordinator: WebRTC call active - reporting connected to CallKit');
526
+ try {
527
+ // Report as connected (CallKit call already answered in UI flow)
528
+ await callkit_1.default.reportCallConnected(callKitUUID);
529
+ console.log('CallKitCoordinator: Call reported as connected to CallKit ', callKitUUID);
530
+ this.connectedCalls.add(callKitUUID);
531
+ }
532
+ catch (error) {
533
+ console.error('CallKitCoordinator: Error reporting call connected:', error);
534
+ }
535
+ }
536
+ break;
537
+ case 'ended':
538
+ case 'failed':
539
+ // Report call ended to CallKit (if not already ended)
540
+ if (!this.endedCalls.has(callKitUUID)) {
541
+ console.log('CallKitCoordinator: Reporting call ended to CallKit');
542
+ const reason = state === 'failed' ? callkit_1.CallEndReason.Failed : callkit_1.CallEndReason.RemoteEnded;
543
+ await callkit_1.default.reportCallEnded(callKitUUID, reason);
544
+ this.endedCalls.add(callKitUUID);
545
+ }
546
+ // Clean up the call mapping
547
+ this.cleanupCall(callKitUUID);
548
+ break;
549
+ case 'ringing':
550
+ // For outgoing calls, we might want to update CallKit with additional info
551
+ // For incoming calls, CallKit already knows about the call
552
+ break;
553
+ }
554
+ };
555
+ call.on('telnyx.call.state', handleStateChange);
556
+ // Store the listener cleanup function
557
+ call._callKitStateListener = () => {
558
+ call.removeListener('telnyx.call.state', handleStateChange);
559
+ };
560
+ }
561
+ /**
562
+ * Clean up call mappings and listeners
563
+ */
564
+ cleanupCall(callKitUUID) {
565
+ // Remove from all tracking sets
566
+ this.processingCalls.delete(callKitUUID);
567
+ this.endedCalls.delete(callKitUUID);
568
+ this.connectedCalls.delete(callKitUUID);
569
+ // Get the call before removing it
570
+ const call = this.callMap.get(callKitUUID);
571
+ // Clean up state listeners
572
+ if (call && call._callKitStateListener) {
573
+ call._callKitStateListener();
574
+ delete call._callKitStateListener;
575
+ }
576
+ // Remove from mapping
577
+ this.callMap.delete(callKitUUID);
578
+ if (call) {
579
+ // Clean up the stored UUID on the call
580
+ delete call._callKitUUID;
581
+ }
582
+ // Reset flags if no more active calls
583
+ if (this.callMap.size === 0) {
584
+ this.resetFlags();
585
+ }
586
+ // Clear VoIP push data now that the call is done
587
+ if (react_native_1.Platform.OS === 'ios') {
588
+ voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush().catch((error) => {
589
+ console.warn('CallKitCoordinator: Error clearing VoIP push data on call cleanup:', error);
590
+ });
591
+ console.log('CallKitCoordinator: ✅ Cleared VoIP push data after call ended');
592
+ }
593
+ }
594
+ /**
595
+ * Get CallKit UUID for a WebRTC call
596
+ */
597
+ getCallKitUUID(call) {
598
+ // First check if the call has the UUID stored on it
599
+ const storedUUID = call._callKitUUID;
600
+ if (storedUUID) {
601
+ return storedUUID;
602
+ }
603
+ // Search through all call mappings
604
+ for (const [uuid, mappedCall] of this.callMap.entries()) {
605
+ if (mappedCall.callId === call.callId) {
606
+ // Store UUID on the call for faster future lookups
607
+ call._callKitUUID = uuid;
608
+ return uuid;
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ /**
614
+ * Get WebRTC call for a CallKit UUID
615
+ */
616
+ getWebRTCCall(callKitUUID) {
617
+ return this.callMap.get(callKitUUID) || null;
618
+ }
619
+ /**
620
+ * Link an existing CallKit call (from push notification) with a WebRTC call
621
+ * This should be called when a WebRTC call arrives that corresponds to an existing CallKit call
622
+ */
623
+ linkExistingCallKitCall(call, callKitUUID) {
624
+ console.log('CallKitCoordinator: Linking existing CallKit call with WebRTC call', {
625
+ callKitUUID,
626
+ webrtcCallId: call.callId,
627
+ });
628
+ // Store the mappings
629
+ this.callMap.set(callKitUUID, call);
630
+ // Store UUID on the call for quick access
631
+ call._callKitUUID = callKitUUID;
632
+ // Set up state listeners
633
+ this.setupWebRTCCallListeners(call, callKitUUID);
634
+ }
635
+ /**
636
+ * Set the VoIP client reference for triggering reconnection
637
+ */
638
+ setVoipClient(voipClient) {
639
+ this.voipClient = voipClient;
640
+ }
641
+ /**
642
+ * Helper method to handle auto-answer logic for push notification calls
643
+ */
644
+ async handleAutoAnswer(call) {
645
+ const shouldAutoAnswer = await async_storage_1.default.getItem('@auto_answer_next_call');
646
+ if (shouldAutoAnswer === 'true') {
647
+ console.log('CallKitCoordinator: Auto-answering call from push notification');
648
+ await async_storage_1.default.removeItem('@auto_answer_next_call');
649
+ // Auto-answer the call after a brief delay to ensure CallKit is ready
650
+ setTimeout(() => {
651
+ call.answer();
652
+ }, 100);
653
+ }
654
+ }
655
+ /**
656
+ * Helper method to clean up push notification state
657
+ */
658
+ async cleanupPushNotificationState() {
659
+ console.log('CallKitCoordinator: ✅ Cleared pending CallKit UUID and auto-answer flag');
660
+ await async_storage_1.default.removeItem('@pending_callkit_uuid');
661
+ await async_storage_1.default.removeItem('@auto_answer_next_call');
662
+ }
663
+ /**
664
+ * Get reference to the SDK client (for queuing actions when call doesn't exist yet)
665
+ */
666
+ getSDKClient() {
667
+ return this.voipClient;
668
+ }
669
+ /**
670
+ * Check if app is in background and disconnect client if no active calls
671
+ */
672
+ async checkBackgroundDisconnection() {
673
+ const currentAppState = react_native_1.AppState.currentState;
674
+ // Only disconnect if app is in background/inactive and no active calls
675
+ if ((currentAppState === 'background' || currentAppState === 'inactive') &&
676
+ this.callMap.size === 0 &&
677
+ this.voipClient) {
678
+ console.log('CallKitCoordinator: App in background with no active calls - disconnecting client');
679
+ try {
680
+ await this.voipClient.logout();
681
+ console.log('CallKitCoordinator: Successfully disconnected client on background');
682
+ }
683
+ catch (error) {
684
+ console.error('CallKitCoordinator: Error disconnecting client on background:', error);
685
+ }
686
+ }
687
+ else {
688
+ console.log('CallKitCoordinator: Skipping background disconnection', {
689
+ appState: currentAppState,
690
+ activeCalls: this.callMap.size,
691
+ hasVoipClient: !!this.voipClient,
692
+ });
693
+ }
694
+ }
695
+ /**
696
+ * Reset only flags (keeping active call mappings intact)
697
+ */
698
+ resetFlags() {
699
+ console.log('CallKitCoordinator: Resetting coordinator flags');
700
+ // Reset push notification flag
701
+ this.isCallFromPush = false;
702
+ console.log('CallKitCoordinator: ✅ Coordinator flags reset');
703
+ }
704
+ /**
705
+ * Check if there are any calls currently being processed by CallKit
706
+ * This helps prevent premature flag resets during CallKit operations
707
+ */
708
+ hasProcessingCalls() {
709
+ return this.processingCalls.size > 0;
710
+ }
711
+ /**
712
+ * Check if there's currently a call from push notification being processed
713
+ * This helps prevent disconnection during push call handling
714
+ */
715
+ getIsCallFromPush() {
716
+ return this.isCallFromPush;
717
+ }
718
+ /**
719
+ * Check if CallKit is available and coordinator is active
720
+ */
721
+ isAvailable() {
722
+ return react_native_1.Platform.OS === 'ios' && callkit_1.default.isAvailable();
723
+ }
724
+ }
725
+ CallKitCoordinator.instance = null;
726
+ // Export singleton instance
727
+ exports.callKitCoordinator = CallKitCoordinator.getInstance();
728
+ exports.default = exports.callKitCoordinator;