@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.
- package/TelnyxVoiceCommons.podspec +32 -0
- package/ios/CallKitBridge.m +44 -0
- package/ios/CallKitBridge.swift +879 -0
- package/ios/README.md +211 -0
- package/ios/VoicePnBridge.m +31 -0
- package/ios/VoicePnBridge.swift +87 -0
- package/lib/callkit/callkit-coordinator.d.ts +126 -0
- package/lib/callkit/callkit-coordinator.js +728 -0
- package/lib/callkit/callkit.d.ts +49 -0
- package/lib/callkit/callkit.js +262 -0
- package/lib/callkit/index.d.ts +4 -0
- package/lib/callkit/index.js +15 -0
- package/lib/callkit/use-callkit-coordinator.d.ts +21 -0
- package/lib/callkit/use-callkit-coordinator.js +53 -0
- package/lib/callkit/use-callkit.d.ts +28 -0
- package/lib/callkit/use-callkit.js +279 -0
- package/lib/context/TelnyxVoiceContext.d.ts +18 -0
- package/lib/context/TelnyxVoiceContext.js +18 -0
- package/lib/hooks/use-callkit-coordinator.d.ts +13 -0
- package/lib/hooks/use-callkit-coordinator.js +48 -0
- package/lib/hooks/useAppReadyNotifier.d.ts +9 -0
- package/lib/hooks/useAppReadyNotifier.js +25 -0
- package/lib/hooks/useAppStateHandler.d.ts +16 -0
- package/lib/hooks/useAppStateHandler.js +105 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +66 -0
- package/lib/internal/CallKitHandler.d.ts +17 -0
- package/lib/internal/CallKitHandler.js +110 -0
- package/lib/internal/callkit-manager.d.ts +69 -0
- package/lib/internal/callkit-manager.js +326 -0
- package/lib/internal/calls/call-state-controller.d.ts +92 -0
- package/lib/internal/calls/call-state-controller.js +294 -0
- package/lib/internal/session/session-manager.d.ts +87 -0
- package/lib/internal/session/session-manager.js +385 -0
- package/lib/internal/user-defaults-helpers.d.ts +10 -0
- package/lib/internal/user-defaults-helpers.js +69 -0
- package/lib/internal/voice-pn-bridge.d.ts +14 -0
- package/lib/internal/voice-pn-bridge.js +5 -0
- package/lib/models/call-state.d.ts +61 -0
- package/lib/models/call-state.js +87 -0
- package/lib/models/call.d.ts +145 -0
- package/lib/models/call.js +372 -0
- package/lib/models/config.d.ts +64 -0
- package/lib/models/config.js +92 -0
- package/lib/models/connection-state.d.ts +34 -0
- package/lib/models/connection-state.js +50 -0
- package/lib/telnyx-voice-app.d.ts +48 -0
- package/lib/telnyx-voice-app.js +486 -0
- package/lib/telnyx-voip-client.d.ts +184 -0
- package/lib/telnyx-voip-client.js +386 -0
- package/package.json +104 -0
- package/src/callkit/callkit-coordinator.ts +846 -0
- package/src/callkit/callkit.ts +322 -0
- package/src/callkit/index.ts +4 -0
- package/src/callkit/use-callkit.ts +345 -0
- package/src/context/TelnyxVoiceContext.tsx +33 -0
- package/src/hooks/use-callkit-coordinator.ts +60 -0
- package/src/hooks/useAppReadyNotifier.ts +25 -0
- package/src/hooks/useAppStateHandler.ts +134 -0
- package/src/index.ts +56 -0
- package/src/internal/CallKitHandler.tsx +149 -0
- package/src/internal/callkit-manager.ts +335 -0
- package/src/internal/calls/call-state-controller.ts +384 -0
- package/src/internal/session/session-manager.ts +467 -0
- package/src/internal/user-defaults-helpers.ts +58 -0
- package/src/internal/voice-pn-bridge.ts +18 -0
- package/src/models/call-state.ts +98 -0
- package/src/models/call.ts +388 -0
- package/src/models/config.ts +125 -0
- package/src/models/connection-state.ts +50 -0
- package/src/telnyx-voice-app.tsx +690 -0
- package/src/telnyx-voip-client.ts +475 -0
- 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;
|