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

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