@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,879 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
#if os(iOS)
|
|
5
|
+
import CallKit
|
|
6
|
+
import AVFoundation
|
|
7
|
+
import UIKit
|
|
8
|
+
import PushKit
|
|
9
|
+
import WebRTC
|
|
10
|
+
|
|
11
|
+
@objc(CallKitBridge)
|
|
12
|
+
class CallKitBridge: RCTEventEmitter {
|
|
13
|
+
|
|
14
|
+
public static var shared: CallKitBridge?
|
|
15
|
+
private var hasListeners = false
|
|
16
|
+
|
|
17
|
+
override init() {
|
|
18
|
+
super.init()
|
|
19
|
+
CallKitBridge.shared = self
|
|
20
|
+
|
|
21
|
+
// Explicitly initialize the CallKit manager when React Native bridge loads
|
|
22
|
+
TelnyxCallKitManager.shared.setup()
|
|
23
|
+
NSLog("TelnyxVoice: CallKitBridge initialized with TelnyxCallKitManager active")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override func supportedEvents() -> [String]! {
|
|
27
|
+
return [
|
|
28
|
+
"CallKitDidReceiveStartCallAction",
|
|
29
|
+
"CallKitDidPerformAnswerCallAction",
|
|
30
|
+
"CallKitDidPerformEndCallAction",
|
|
31
|
+
"CallKitDidReceivePush",
|
|
32
|
+
"AudioSessionActivated",
|
|
33
|
+
"AudioSessionDeactivated",
|
|
34
|
+
"AudioSessionFailed",
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override func startObserving() {
|
|
39
|
+
hasListeners = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override func stopObserving() {
|
|
43
|
+
hasListeners = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Helper to get the CallKit manager
|
|
47
|
+
private func getCallKitManager() -> TelnyxCallKitManager {
|
|
48
|
+
return TelnyxCallKitManager.shared
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Direct event emission methods called by TelnyxVoiceAppDelegate
|
|
52
|
+
public func emitCallEvent(_ eventName: String, callUUID: UUID, callData: [String: Any]?) {
|
|
53
|
+
guard hasListeners else { return }
|
|
54
|
+
|
|
55
|
+
let eventData: [String: Any] = [
|
|
56
|
+
"callUUID": callUUID.uuidString,
|
|
57
|
+
"callData": callData ?? [:],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
sendEvent(withName: eventName, body: eventData)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Event emission method for audio session changes
|
|
64
|
+
public func emitAudioSessionEvent(_ eventName: String, data: [String: Any]) {
|
|
65
|
+
guard hasListeners else { return }
|
|
66
|
+
|
|
67
|
+
sendEvent(withName: eventName, body: data)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: - React Native Exported Methods
|
|
71
|
+
|
|
72
|
+
@objc func startOutgoingCall(
|
|
73
|
+
_ callUUID: String, handle: String, displayName: String,
|
|
74
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
75
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
76
|
+
) {
|
|
77
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
78
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let manager = getCallKitManager()
|
|
83
|
+
guard let callController = manager.callKitController else {
|
|
84
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let callHandle = CXHandle(type: .phoneNumber, value: handle)
|
|
89
|
+
let startCallAction = CXStartCallAction(call: uuid, handle: callHandle)
|
|
90
|
+
let transaction = CXTransaction(action: startCallAction)
|
|
91
|
+
|
|
92
|
+
callController.request(transaction) { error in
|
|
93
|
+
if let error = error {
|
|
94
|
+
reject("START_CALL_ERROR", error.localizedDescription, error)
|
|
95
|
+
} else {
|
|
96
|
+
resolve(["success": true, "callUUID": callUUID])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@objc func reportIncomingCall(
|
|
102
|
+
_ callUUID: String, handle: String, displayName: String,
|
|
103
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
104
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
105
|
+
) {
|
|
106
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
107
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let manager = getCallKitManager()
|
|
112
|
+
guard let provider = manager.callKitProvider else {
|
|
113
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let callHandle = CXHandle(type: .phoneNumber, value: handle)
|
|
118
|
+
let callUpdate = CXCallUpdate()
|
|
119
|
+
callUpdate.remoteHandle = callHandle
|
|
120
|
+
callUpdate.hasVideo = false
|
|
121
|
+
callUpdate.localizedCallerName = displayName
|
|
122
|
+
|
|
123
|
+
provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
|
|
124
|
+
if let error = error {
|
|
125
|
+
reject("INCOMING_CALL_ERROR", error.localizedDescription, error)
|
|
126
|
+
} else {
|
|
127
|
+
resolve(["success": true, "callUUID": callUUID])
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@objc func endCall(
|
|
133
|
+
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
134
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
135
|
+
) {
|
|
136
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
137
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let manager = getCallKitManager()
|
|
142
|
+
guard let callController = manager.callKitController else {
|
|
143
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let endCallAction = CXEndCallAction(call: uuid)
|
|
148
|
+
let transaction = CXTransaction(action: endCallAction)
|
|
149
|
+
|
|
150
|
+
callController.request(transaction) { error in
|
|
151
|
+
if let error = error {
|
|
152
|
+
reject("END_CALL_ERROR", error.localizedDescription, error)
|
|
153
|
+
} else {
|
|
154
|
+
resolve(["success": true, "callUUID": callUUID])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@objc func answerCall(
|
|
160
|
+
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
161
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
162
|
+
) {
|
|
163
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
164
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let manager = getCallKitManager()
|
|
169
|
+
guard let callController = manager.callKitController else {
|
|
170
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Mark this call as already answered to prevent duplicate emission
|
|
175
|
+
if var callData = manager.activeCalls[uuid] {
|
|
176
|
+
callData["isAlreadyAnswered"] = true
|
|
177
|
+
manager.activeCalls[uuid] = callData
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let answerCallAction = CXAnswerCallAction(call: uuid)
|
|
181
|
+
let transaction = CXTransaction(action: answerCallAction)
|
|
182
|
+
|
|
183
|
+
callController.request(transaction) { error in
|
|
184
|
+
if let error = error {
|
|
185
|
+
reject("ANSWER_CALL_ERROR", error.localizedDescription, error)
|
|
186
|
+
} else {
|
|
187
|
+
resolve(["success": true, "callUUID": callUUID])
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@objc func reportCallConnected(
|
|
193
|
+
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
194
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
195
|
+
) {
|
|
196
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
197
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let manager = getCallKitManager()
|
|
202
|
+
guard let provider = manager.callKitProvider else {
|
|
203
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
provider.reportOutgoingCall(with: uuid, connectedAt: Date())
|
|
208
|
+
resolve(["success": true])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@objc func reportCallEnded(
|
|
212
|
+
_ callUUID: String, reason: NSNumber,
|
|
213
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
214
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
215
|
+
) {
|
|
216
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
217
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let manager = getCallKitManager()
|
|
222
|
+
guard let provider = manager.callKitProvider else {
|
|
223
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let endReason = CXCallEndedReason(rawValue: reason.intValue) ?? .remoteEnded
|
|
228
|
+
provider.reportCall(with: uuid, endedAt: Date(), reason: endReason)
|
|
229
|
+
resolve(["success": true])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@objc func getActiveCalls(
|
|
233
|
+
_ resolver: @escaping RCTPromiseResolveBlock,
|
|
234
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
235
|
+
) {
|
|
236
|
+
let manager = getCallKitManager()
|
|
237
|
+
let activeCalls = Array(manager.activeCalls.values)
|
|
238
|
+
resolver(activeCalls)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@objc func updateCall(
|
|
242
|
+
_ callUUID: String, displayName: String, handle: String,
|
|
243
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
244
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
245
|
+
) {
|
|
246
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
247
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let manager = getCallKitManager()
|
|
252
|
+
guard let provider = manager.callKitProvider else {
|
|
253
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let callHandle = CXHandle(type: .phoneNumber, value: handle)
|
|
258
|
+
let callUpdate = CXCallUpdate()
|
|
259
|
+
callUpdate.remoteHandle = callHandle
|
|
260
|
+
callUpdate.localizedCallerName = displayName
|
|
261
|
+
|
|
262
|
+
provider.reportCall(with: uuid, updated: callUpdate)
|
|
263
|
+
resolve(["success": true])
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// MARK: - TelnyxCallKitManager
|
|
268
|
+
/// TelnyxCallKitManager - Automatically handles CallKit integration for Telnyx Voice
|
|
269
|
+
/// This class sets up CallKit and VoIP push notifications automatically when the app starts
|
|
270
|
+
/// No AppDelegate changes required!
|
|
271
|
+
@objc public class TelnyxCallKitManager: NSObject {
|
|
272
|
+
|
|
273
|
+
public static let shared = TelnyxCallKitManager()
|
|
274
|
+
|
|
275
|
+
public var voipRegistry: PKPushRegistry?
|
|
276
|
+
public var callKitProvider: CXProvider?
|
|
277
|
+
public var callKitController: CXCallController?
|
|
278
|
+
public var activeCalls: [UUID: [String: Any]] = [:]
|
|
279
|
+
|
|
280
|
+
private override init() {
|
|
281
|
+
super.init()
|
|
282
|
+
// Only setup if explicitly requested via setup() method
|
|
283
|
+
// This prevents automatic initialization conflicts
|
|
284
|
+
NSLog("TelnyxVoice: TelnyxCallKitManager initialized (not yet active)")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public func setup() {
|
|
288
|
+
// Only initialize once
|
|
289
|
+
guard callKitProvider == nil else {
|
|
290
|
+
NSLog("TelnyxVoice: CallKit already setup, skipping")
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
DispatchQueue.main.async {
|
|
295
|
+
self.setupAutomatically()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public func setupSynchronously() {
|
|
300
|
+
// Only initialize once
|
|
301
|
+
guard callKitProvider == nil else {
|
|
302
|
+
NSLog("TelnyxVoice: CallKit already setup, skipping")
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// CRITICAL: Setup CallKit synchronously for terminated app VoIP push handling
|
|
307
|
+
// This MUST happen in the same run loop as the VoIP push
|
|
308
|
+
NSLog("TelnyxVoice: Synchronous CallKit setup for terminated app scenario...")
|
|
309
|
+
setupCallKit()
|
|
310
|
+
NSLog("TelnyxVoice: ✅ CallKit provider ready for terminated app handling")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private func setupAutomatically() {
|
|
314
|
+
NSLog("TelnyxVoice: Auto-initializing CallKit via TelnyxCallKitManager...")
|
|
315
|
+
// Note: VoIP push registration is handled by AppDelegate, not here
|
|
316
|
+
// setupVoIPPushNotifications()
|
|
317
|
+
setupCallKit()
|
|
318
|
+
observeAppDelegate()
|
|
319
|
+
NSLog("TelnyxVoice: ✅ TelnyxCallKitManager is now the ACTIVE CallKit provider")
|
|
320
|
+
NSLog("TelnyxVoice: ⚠️ VoIP push handled by AppDelegate, not TelnyxCallKitManager")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func setupVoIPPushNotifications() {
|
|
324
|
+
voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
|
|
325
|
+
voipRegistry?.delegate = self
|
|
326
|
+
voipRegistry?.desiredPushTypes = [PKPushType.voIP]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private func setupCallKit() {
|
|
330
|
+
// CRITICAL: Configure WebRTC for manual audio control BEFORE CallKit setup
|
|
331
|
+
RTCAudioSession.sharedInstance().useManualAudio = true
|
|
332
|
+
RTCAudioSession.sharedInstance().isAudioEnabled = false // MUST be false initially!
|
|
333
|
+
NSLog(
|
|
334
|
+
"🎧 TelnyxVoice: WebRTC configured for manual audio control (audio DISABLED until CallKit activates)"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// Use the localizedName from the app's bundle display name or fallback
|
|
338
|
+
let appName =
|
|
339
|
+
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
|
340
|
+
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
|
|
341
|
+
?? "Voice Call"
|
|
342
|
+
|
|
343
|
+
let configuration = CXProviderConfiguration(localizedName: appName)
|
|
344
|
+
configuration.supportsVideo = false
|
|
345
|
+
configuration.maximumCallGroups = 1
|
|
346
|
+
configuration.maximumCallsPerCallGroup = 1
|
|
347
|
+
configuration.supportedHandleTypes = [.phoneNumber, .generic]
|
|
348
|
+
configuration.includesCallsInRecents = true
|
|
349
|
+
|
|
350
|
+
callKitProvider = CXProvider(configuration: configuration)
|
|
351
|
+
callKitProvider?.setDelegate(self, queue: nil)
|
|
352
|
+
callKitController = CXCallController()
|
|
353
|
+
|
|
354
|
+
NSLog(
|
|
355
|
+
"📞 TelnyxVoice: CallKit provider instance: \(String(describing: callKitProvider))")
|
|
356
|
+
NSLog(
|
|
357
|
+
"📞 TelnyxVoice: CallKit controller instance: \(String(describing: callKitController))"
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func configureAudioSession() {
|
|
362
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
363
|
+
do {
|
|
364
|
+
try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
|
|
365
|
+
NSLog("Succeeded to activate audio session")
|
|
366
|
+
} catch {
|
|
367
|
+
NSLog("Failed to activate audio session: \(error)")
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private func observeAppDelegate() {
|
|
372
|
+
// Automatically hook into app lifecycle if needed
|
|
373
|
+
NotificationCenter.default.addObserver(
|
|
374
|
+
self,
|
|
375
|
+
selector: #selector(appDidBecomeActive),
|
|
376
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
377
|
+
object: nil
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@objc private func appDidBecomeActive() {
|
|
382
|
+
// Ensure CallKit is still properly configured
|
|
383
|
+
if callKitProvider == nil {
|
|
384
|
+
setupCallKit()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// MARK: - PKPushRegistryDelegate
|
|
390
|
+
extension TelnyxCallKitManager: PKPushRegistryDelegate {
|
|
391
|
+
|
|
392
|
+
public func pushRegistry(
|
|
393
|
+
_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials,
|
|
394
|
+
for type: PKPushType
|
|
395
|
+
) {
|
|
396
|
+
NSLog("TelnyxVoice: VoIP push token updated")
|
|
397
|
+
let deviceToken = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
|
|
398
|
+
NSLog("TelnyxVoice: VoIP Push Token: \(deviceToken)")
|
|
399
|
+
|
|
400
|
+
// Store token in both keys for compatibility
|
|
401
|
+
UserDefaults.standard.set(deviceToken, forKey: "telnyx_voip_push_token")
|
|
402
|
+
UserDefaults.standard.set(deviceToken, forKey: "voip_push_token") // For VoicePnBridge compatibility
|
|
403
|
+
UserDefaults.standard.synchronize()
|
|
404
|
+
|
|
405
|
+
// Forward the token to RNVoipPushNotificationManager for React Native compatibility
|
|
406
|
+
if let RNVoipPushNotificationManager = NSClassFromString(
|
|
407
|
+
"RNVoipPushNotificationManager") as? NSObject.Type
|
|
408
|
+
{
|
|
409
|
+
if RNVoipPushNotificationManager.responds(
|
|
410
|
+
to: Selector(("didUpdatePushCredentials:forType:")))
|
|
411
|
+
{
|
|
412
|
+
RNVoipPushNotificationManager.perform(
|
|
413
|
+
Selector(("didUpdatePushCredentials:forType:")),
|
|
414
|
+
with: pushCredentials,
|
|
415
|
+
with: type.rawValue
|
|
416
|
+
)
|
|
417
|
+
NSLog("TelnyxVoice: Forwarded token to RNVoipPushNotificationManager")
|
|
418
|
+
} else {
|
|
419
|
+
NSLog(
|
|
420
|
+
"TelnyxVoice: RNVoipPushNotificationManager doesn't respond to didUpdatePushCredentials"
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
NSLog("TelnyxVoice: RNVoipPushNotificationManager class not found")
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public func pushRegistry(
|
|
429
|
+
_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType
|
|
430
|
+
) {
|
|
431
|
+
NSLog("TelnyxVoice: VoIP push token invalidated")
|
|
432
|
+
UserDefaults.standard.removeObject(forKey: "telnyx_voip_push_token")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public func pushRegistry(
|
|
436
|
+
_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
|
|
437
|
+
for type: PKPushType, completion: @escaping () -> Void
|
|
438
|
+
) {
|
|
439
|
+
NSLog("TelnyxVoice: Received VoIP push notification: \(payload.dictionaryPayload)")
|
|
440
|
+
|
|
441
|
+
// Store the VoIP push data for VoicePnBridge (same format as AppDelegate was using)
|
|
442
|
+
do {
|
|
443
|
+
let jsonData = try JSONSerialization.data(withJSONObject: payload.dictionaryPayload)
|
|
444
|
+
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
|
445
|
+
|
|
446
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
447
|
+
UserDefaults.standard.set(jsonString, forKey: "pending_push_metadata")
|
|
448
|
+
UserDefaults.standard.synchronize()
|
|
449
|
+
NSLog("TelnyxVoice: Stored VoIP push data for VoicePnBridge")
|
|
450
|
+
} catch {
|
|
451
|
+
NSLog("TelnyxVoice: Error converting VoIP payload to JSON: \(error)")
|
|
452
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
453
|
+
UserDefaults.standard.set("{}", forKey: "pending_push_metadata")
|
|
454
|
+
UserDefaults.standard.synchronize()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let callUUID = UUID()
|
|
458
|
+
var caller = "Unknown Caller"
|
|
459
|
+
var callId: String?
|
|
460
|
+
|
|
461
|
+
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {
|
|
462
|
+
caller =
|
|
463
|
+
metadata["caller_name"] as? String ?? metadata["caller_number"] as? String
|
|
464
|
+
?? "Unknown Caller"
|
|
465
|
+
callId = metadata["call_id"] as? String
|
|
466
|
+
} else {
|
|
467
|
+
caller =
|
|
468
|
+
payload.dictionaryPayload["caller"] as? String ?? payload.dictionaryPayload[
|
|
469
|
+
"from"] as? String ?? "Unknown Caller"
|
|
470
|
+
callId =
|
|
471
|
+
payload.dictionaryPayload["call_id"] as? String ?? payload.dictionaryPayload[
|
|
472
|
+
"callId"] as? String
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
activeCalls[callUUID] = [
|
|
476
|
+
"caller": caller,
|
|
477
|
+
"callId": callId as Any,
|
|
478
|
+
"payload": payload.dictionaryPayload,
|
|
479
|
+
"uuid": callUUID.uuidString,
|
|
480
|
+
"direction": "incoming",
|
|
481
|
+
"source": "push",
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
let handle = CXHandle(type: .phoneNumber, value: caller)
|
|
485
|
+
let callUpdate = CXCallUpdate()
|
|
486
|
+
callUpdate.remoteHandle = handle
|
|
487
|
+
callUpdate.hasVideo = false
|
|
488
|
+
callUpdate.localizedCallerName = caller
|
|
489
|
+
|
|
490
|
+
callKitProvider?.reportNewIncomingCall(with: callUUID, update: callUpdate) {
|
|
491
|
+
[weak self] error in
|
|
492
|
+
if let error = error {
|
|
493
|
+
NSLog("TelnyxVoice: CallKit error: \(error.localizedDescription)")
|
|
494
|
+
self?.activeCalls.removeValue(forKey: callUUID)
|
|
495
|
+
} else {
|
|
496
|
+
NSLog("TelnyxVoice: CallKit incoming call reported successfully")
|
|
497
|
+
}
|
|
498
|
+
completion()
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// MARK: - CXProviderDelegate
|
|
504
|
+
extension TelnyxCallKitManager: CXProviderDelegate {
|
|
505
|
+
|
|
506
|
+
public func providerDidReset(_ provider: CXProvider) {
|
|
507
|
+
NSLog("📞 TelnyxVoice: CALLKIT PROVIDER RESET - Provider: \(provider)")
|
|
508
|
+
NSLog("TelnyxVoice: CallKit provider reset - ending all active calls")
|
|
509
|
+
activeCalls.removeAll()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
|
513
|
+
NSLog("📞 TelnyxVoice: CALLKIT ANSWER ACTION - Provider: \(provider), Action: \(action)")
|
|
514
|
+
NSLog("TelnyxVoice: User answered call with UUID: \(action.callUUID)")
|
|
515
|
+
|
|
516
|
+
// Check if this is a programmatic answer (call already answered in WebRTC)
|
|
517
|
+
// vs a user answer from CallKit UI
|
|
518
|
+
if let callData = activeCalls[action.callUUID],
|
|
519
|
+
let isAlreadyAnswered = callData["isAlreadyAnswered"] as? Bool,
|
|
520
|
+
isAlreadyAnswered
|
|
521
|
+
{
|
|
522
|
+
NSLog("TelnyxVoice: Call already answered in WebRTC, skipping emission")
|
|
523
|
+
} else {
|
|
524
|
+
// Notify React Native via CallKit bridge (only for user-initiated answers)
|
|
525
|
+
CallKitBridge.shared?.emitCallEvent(
|
|
526
|
+
"CallKitDidPerformAnswerCallAction", callUUID: action.callUUID,
|
|
527
|
+
callData: activeCalls[action.callUUID])
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
NSLog("📞 TelnyxVoice: Fulfilling CXAnswerCallAction for call UUID: \(action.callUUID)")
|
|
531
|
+
action.fulfill()
|
|
532
|
+
NSLog("📞 TelnyxVoice: ✅ CXAnswerCallAction fulfilled successfully")
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
536
|
+
NSLog("TelnyxVoice: User ended call with UUID: \(action.callUUID)")
|
|
537
|
+
|
|
538
|
+
// Notify React Native via CallKit bridge
|
|
539
|
+
CallKitBridge.shared?.emitCallEvent(
|
|
540
|
+
"CallKitDidPerformEndCallAction", callUUID: action.callUUID,
|
|
541
|
+
callData: activeCalls[action.callUUID])
|
|
542
|
+
|
|
543
|
+
activeCalls.removeValue(forKey: action.callUUID)
|
|
544
|
+
NSLog("📞 TelnyxVoice: Fulfilling CXEndCallAction for call UUID: \(action.callUUID)")
|
|
545
|
+
action.fulfill()
|
|
546
|
+
NSLog("📞 TelnyxVoice: ✅ CXEndCallAction fulfilled successfully")
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
550
|
+
NSLog("TelnyxVoice: Starting outgoing call with UUID: \(action.callUUID)")
|
|
551
|
+
|
|
552
|
+
// Notify React Native via CallKit bridge
|
|
553
|
+
CallKitBridge.shared?.emitCallEvent(
|
|
554
|
+
"CallKitDidReceiveStartCallAction", callUUID: action.callUUID,
|
|
555
|
+
callData: activeCalls[action.callUUID])
|
|
556
|
+
|
|
557
|
+
NSLog("📞 TelnyxVoice: Fulfilling CXStartCallAction for call UUID: \(action.callUUID)")
|
|
558
|
+
action.fulfill()
|
|
559
|
+
NSLog("📞 TelnyxVoice: ✅ CXStartCallAction fulfilled successfully")
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
563
|
+
NSLog(
|
|
564
|
+
"🎧🎧🎧 TelnyxVoice: AUDIO SESSION ACTIVATED BY CALLKIT - USER ANSWERED THE CALL! 🎧🎧🎧")
|
|
565
|
+
NSLog("🎧 Provider: \(provider)")
|
|
566
|
+
NSLog(
|
|
567
|
+
"🎧 Audio session details: active=\(audioSession.isOtherAudioPlaying), category=\(audioSession.category.rawValue), mode=\(audioSession.mode.rawValue)"
|
|
568
|
+
)
|
|
569
|
+
NSLog(
|
|
570
|
+
"🎧 Current RTCAudioSession state - useManualAudio=\(RTCAudioSession.sharedInstance().useManualAudio), isAudioEnabled=\(RTCAudioSession.sharedInstance().isAudioEnabled)"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
// CRITICAL: Activate WebRTC audio session (matches Flutter implementation)
|
|
574
|
+
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
|
|
575
|
+
RTCAudioSession.sharedInstance().isAudioEnabled = true
|
|
576
|
+
NSLog("🎧 TelnyxVoice: WebRTC RTCAudioSession activated and audio enabled")
|
|
577
|
+
|
|
578
|
+
do {
|
|
579
|
+
// Configure audio session for VoIP with proper routing
|
|
580
|
+
try audioSession.setCategory(
|
|
581
|
+
.playAndRecord, mode: .voiceChat,
|
|
582
|
+
options: [.allowBluetooth, .allowBluetoothA2DP])
|
|
583
|
+
try audioSession.setActive(true)
|
|
584
|
+
|
|
585
|
+
// Emit audio session activated event to React Native
|
|
586
|
+
CallKitBridge.shared?.emitAudioSessionEvent(
|
|
587
|
+
"AudioSessionActivated",
|
|
588
|
+
data: [
|
|
589
|
+
"category": audioSession.category.rawValue,
|
|
590
|
+
"mode": audioSession.mode.rawValue,
|
|
591
|
+
"isActive": true,
|
|
592
|
+
])
|
|
593
|
+
|
|
594
|
+
NSLog(
|
|
595
|
+
"🎧 SUCCESS: Audio session ACTIVE for VoIP - Category: \(audioSession.category.rawValue), Mode: \(audioSession.mode.rawValue)"
|
|
596
|
+
)
|
|
597
|
+
} catch {
|
|
598
|
+
NSLog("❌ FAILED: Audio session configuration error: \(error)")
|
|
599
|
+
|
|
600
|
+
// Emit audio session failed event to React Native
|
|
601
|
+
CallKitBridge.shared?.emitAudioSessionEvent(
|
|
602
|
+
"AudioSessionFailed",
|
|
603
|
+
data: [
|
|
604
|
+
"error": error.localizedDescription
|
|
605
|
+
])
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
|
610
|
+
NSLog("🔇🔇🔇 TelnyxVoice: AUDIO SESSION DEACTIVATED BY CALLKIT 🔇🔇🔇")
|
|
611
|
+
NSLog("🔇 Provider: \(provider)")
|
|
612
|
+
NSLog(
|
|
613
|
+
"🔇 Audio session details: active=\(audioSession.isOtherAudioPlaying), category=\(audioSession.category.rawValue), mode=\(audioSession.mode.rawValue)"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
// CRITICAL: Deactivate WebRTC audio session (matches Flutter implementation)
|
|
617
|
+
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
|
|
618
|
+
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
|
619
|
+
NSLog("🔇 TelnyxVoice: WebRTC RTCAudioSession deactivated and audio disabled")
|
|
620
|
+
|
|
621
|
+
// Emit audio session deactivated event to React Native
|
|
622
|
+
CallKitBridge.shared?.emitAudioSessionEvent(
|
|
623
|
+
"AudioSessionDeactivated",
|
|
624
|
+
data: [
|
|
625
|
+
"category": audioSession.category.rawValue,
|
|
626
|
+
"mode": audioSession.mode.rawValue,
|
|
627
|
+
"isActive": false,
|
|
628
|
+
])
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// MARK: - VoIP Push Handler
|
|
633
|
+
// This class provides VoIP push notification handling for the main AppDelegate
|
|
634
|
+
// It integrates with the react-voice-commons CallKit system
|
|
635
|
+
|
|
636
|
+
@objc public class TelnyxVoipPushHandler: NSObject {
|
|
637
|
+
|
|
638
|
+
@objc public static let shared = TelnyxVoipPushHandler()
|
|
639
|
+
|
|
640
|
+
private override init() {
|
|
641
|
+
super.init()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Initialize VoIP push registration
|
|
646
|
+
* Call this from the main app's AppDelegate didFinishLaunchingWithOptions
|
|
647
|
+
*/
|
|
648
|
+
@objc public static func initializeVoipRegistration() {
|
|
649
|
+
if let RNVoipPushNotificationManager = NSClassFromString(
|
|
650
|
+
"RNVoipPushNotificationManager") as? NSObject.Type
|
|
651
|
+
{
|
|
652
|
+
if RNVoipPushNotificationManager.responds(to: Selector(("voipRegistration"))) {
|
|
653
|
+
RNVoipPushNotificationManager.perform(Selector(("voipRegistration")))
|
|
654
|
+
NSLog("[TelnyxVoipPushHandler] VoIP registration initialized")
|
|
655
|
+
} else {
|
|
656
|
+
NSLog(
|
|
657
|
+
"[TelnyxVoipPushHandler] RNVoipPushNotificationManager doesn't respond to voipRegistration"
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
NSLog("[TelnyxVoipPushHandler] RNVoipPushNotificationManager class not found")
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Call this method from your AppDelegate's didReceiveIncomingPushWith method
|
|
667
|
+
* to handle VoIP push notifications using the react-voice-commons CallKit system
|
|
668
|
+
*/
|
|
669
|
+
@objc public func handleVoipPush(
|
|
670
|
+
_ payload: PKPushPayload,
|
|
671
|
+
type: PKPushType,
|
|
672
|
+
completion: @escaping () -> Void
|
|
673
|
+
) {
|
|
674
|
+
NSLog("[TelnyxVoipPushHandler] VoIP push received for type: \(type.rawValue)")
|
|
675
|
+
NSLog("[TelnyxVoipPushHandler] VoIP payload: \(payload.dictionaryPayload)")
|
|
676
|
+
|
|
677
|
+
// Configure audio session early for VoIP call
|
|
678
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
679
|
+
do {
|
|
680
|
+
try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
|
|
681
|
+
NSLog("Succeeded to activate audio session")
|
|
682
|
+
} catch {
|
|
683
|
+
NSLog("Failed to activate audio session: \(error)")
|
|
684
|
+
}
|
|
685
|
+
// Store the VoIP push data for VoicePnBridge
|
|
686
|
+
do {
|
|
687
|
+
let jsonData = try JSONSerialization.data(withJSONObject: payload.dictionaryPayload)
|
|
688
|
+
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
|
689
|
+
|
|
690
|
+
// Store for TelnyxVoiceApp (push action flow)
|
|
691
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
692
|
+
UserDefaults.standard.set(jsonString, forKey: "pending_push_metadata")
|
|
693
|
+
|
|
694
|
+
// ALSO store for VoicePnBridge.getPendingVoipPush() (CallKit coordinator flow)
|
|
695
|
+
// This creates a structured object with payload property
|
|
696
|
+
let voipPushData = [
|
|
697
|
+
"payload": payload.dictionaryPayload
|
|
698
|
+
]
|
|
699
|
+
let voipJsonData = try JSONSerialization.data(withJSONObject: voipPushData)
|
|
700
|
+
let voipJsonString = String(data: voipJsonData, encoding: .utf8) ?? ""
|
|
701
|
+
UserDefaults.standard.set(voipJsonString, forKey: "pending_voip_push")
|
|
702
|
+
|
|
703
|
+
UserDefaults.standard.synchronize()
|
|
704
|
+
NSLog(
|
|
705
|
+
"[TelnyxVoipPushHandler] Stored VoIP push data for both TelnyxVoiceApp and VoicePnBridge"
|
|
706
|
+
)
|
|
707
|
+
} catch {
|
|
708
|
+
NSLog("[TelnyxVoipPushHandler] Error converting VoIP payload to JSON: \(error)")
|
|
709
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
710
|
+
UserDefaults.standard.set("{}", forKey: "pending_push_metadata")
|
|
711
|
+
UserDefaults.standard.set("{\"payload\":{}}", forKey: "pending_voip_push")
|
|
712
|
+
UserDefaults.standard.synchronize()
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Extract caller information and call_id from the payload
|
|
716
|
+
var callerName = "Unknown Caller"
|
|
717
|
+
var callerNumber = "Unknown"
|
|
718
|
+
var callId: String?
|
|
719
|
+
|
|
720
|
+
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {
|
|
721
|
+
callerName =
|
|
722
|
+
metadata["caller_name"] as? String ?? metadata["caller_number"] as? String
|
|
723
|
+
?? "Unknown Caller"
|
|
724
|
+
callerNumber = metadata["caller_number"] as? String ?? callerName
|
|
725
|
+
callId = metadata["call_id"] as? String
|
|
726
|
+
} else {
|
|
727
|
+
callerName =
|
|
728
|
+
payload.dictionaryPayload["caller"] as? String ?? payload.dictionaryPayload[
|
|
729
|
+
"from"] as? String ?? "Unknown Caller"
|
|
730
|
+
callerNumber = payload.dictionaryPayload["caller_number"] as? String ?? callerName
|
|
731
|
+
callId = payload.dictionaryPayload["call_id"] as? String
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Use call_id as CallKit UUID to ensure matching with WebRTC
|
|
735
|
+
guard let callIdString = callId, let callUUID = UUID(uuidString: callIdString) else {
|
|
736
|
+
NSLog(
|
|
737
|
+
"[TelnyxVoipPushHandler] ❌ No valid call_id found in payload, cannot process call"
|
|
738
|
+
)
|
|
739
|
+
completion()
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
NSLog(
|
|
744
|
+
"[TelnyxVoipPushHandler] Processing call - Call ID as UUID: \(callUUID.uuidString), Caller: \(callerName), Number: \(callerNumber)"
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
// Use unified CallKit handling path that works for both running and terminated app scenarios
|
|
748
|
+
NSLog("[TelnyxVoipPushHandler] Using unified CallKit handling path")
|
|
749
|
+
|
|
750
|
+
// Use the existing TelnyxCallKitManager which has CallKit setup
|
|
751
|
+
let callKitManager = TelnyxCallKitManager.shared
|
|
752
|
+
|
|
753
|
+
// CRITICAL: Setup CallKit SYNCHRONOUSLY - no async dispatch allowed
|
|
754
|
+
// This must happen in the same run loop as the VoIP push
|
|
755
|
+
callKitManager.setupSynchronously()
|
|
756
|
+
|
|
757
|
+
// Ensure we have a valid CallKit provider after setup
|
|
758
|
+
guard let callKitProvider = callKitManager.callKitProvider else {
|
|
759
|
+
NSLog(
|
|
760
|
+
"[TelnyxVoipPushHandler] ❌ FATAL: CallKit provider not available after synchronous setup!"
|
|
761
|
+
)
|
|
762
|
+
completion()
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
NSLog("[TelnyxVoipPushHandler] ✅ CallKit provider ready, reporting incoming call")
|
|
767
|
+
|
|
768
|
+
// Configure audio session before reporting the call
|
|
769
|
+
//callKitManager.configureAudioSession()
|
|
770
|
+
|
|
771
|
+
// Store call data manually and report to CallKit
|
|
772
|
+
let isAppRunning = CallKitBridge.shared != nil
|
|
773
|
+
callKitManager.activeCalls[callUUID] = [
|
|
774
|
+
"caller": callerName,
|
|
775
|
+
"handle": callerNumber,
|
|
776
|
+
"payload": payload.dictionaryPayload,
|
|
777
|
+
"uuid": callUUID.uuidString,
|
|
778
|
+
"direction": "incoming",
|
|
779
|
+
"source": isAppRunning ? "push_notification" : "terminated_app_push",
|
|
780
|
+
]
|
|
781
|
+
|
|
782
|
+
// Report to CallKit immediately
|
|
783
|
+
let handle = CXHandle(type: .phoneNumber, value: callerNumber)
|
|
784
|
+
let callUpdate = CXCallUpdate()
|
|
785
|
+
callUpdate.remoteHandle = handle
|
|
786
|
+
callUpdate.hasVideo = false
|
|
787
|
+
callUpdate.localizedCallerName = callerName
|
|
788
|
+
|
|
789
|
+
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
|
|
790
|
+
if let error = error {
|
|
791
|
+
NSLog(
|
|
792
|
+
"[TelnyxVoipPushHandler] ❌ CallKit error during terminated app handling: \(error.localizedDescription)"
|
|
793
|
+
)
|
|
794
|
+
callKitManager.activeCalls.removeValue(forKey: callUUID)
|
|
795
|
+
} else {
|
|
796
|
+
NSLog(
|
|
797
|
+
"[TelnyxVoipPushHandler] ✅ CallKit call reported successfully via unified path"
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
// CRITICAL: Store the CallKit UUID for the React Native CallKitCoordinator
|
|
801
|
+
// This allows the coordinator to find the existing CallKit call instead of creating a duplicate
|
|
802
|
+
UserDefaults.standard.set(callUUID.uuidString, forKey: "@pending_callkit_uuid")
|
|
803
|
+
UserDefaults.standard.synchronize()
|
|
804
|
+
NSLog(
|
|
805
|
+
"[TelnyxVoipPushHandler] ✅ Stored CallKit UUID for React Native coordinator: \(callUUID.uuidString)"
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
// Try to emit push received event if CallKit bridge is available
|
|
809
|
+
if let callKitBridge = CallKitBridge.shared {
|
|
810
|
+
let source = isAppRunning ? "push_notification" : "terminated_app_push"
|
|
811
|
+
callKitBridge.emitCallEvent(
|
|
812
|
+
"CallKitDidReceivePush", callUUID: callUUID,
|
|
813
|
+
callData: [
|
|
814
|
+
"caller": callerName,
|
|
815
|
+
"handle": callerNumber,
|
|
816
|
+
"payload": payload.dictionaryPayload,
|
|
817
|
+
"uuid": callUUID.uuidString,
|
|
818
|
+
"direction": "incoming",
|
|
819
|
+
"source": source,
|
|
820
|
+
])
|
|
821
|
+
NSLog(
|
|
822
|
+
"[TelnyxVoipPushHandler] ✅ Emitted CallKitDidReceivePush event to React Native"
|
|
823
|
+
)
|
|
824
|
+
} else {
|
|
825
|
+
NSLog(
|
|
826
|
+
"[TelnyxVoipPushHandler] ⚠️ React Native bridge not ready - event will be emitted when bridge becomes available"
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
completion()
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Call this method from your AppDelegate's didUpdate pushCredentials method
|
|
837
|
+
* to handle VoIP token updates
|
|
838
|
+
*/
|
|
839
|
+
@objc public func handleVoipTokenUpdate(
|
|
840
|
+
_ pushCredentials: PKPushCredentials,
|
|
841
|
+
type: PKPushType
|
|
842
|
+
) {
|
|
843
|
+
NSLog(
|
|
844
|
+
"[TelnyxVoipPushHandler] VoIP push credentials updated for type: \(type.rawValue)")
|
|
845
|
+
|
|
846
|
+
let tokenString = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
|
|
847
|
+
NSLog("[TelnyxVoipPushHandler] VoIP token: \(tokenString)")
|
|
848
|
+
|
|
849
|
+
// Store token in UserDefaults for VoicePnBridge access
|
|
850
|
+
UserDefaults.standard.set(tokenString, forKey: "voip_push_token")
|
|
851
|
+
UserDefaults.standard.synchronize()
|
|
852
|
+
NSLog("[TelnyxVoipPushHandler] VoIP Push Token stored in UserDefaults")
|
|
853
|
+
|
|
854
|
+
// Forward to RNVoipPushNotificationManager for React Native compatibility
|
|
855
|
+
if let RNVoipPushNotificationManager = NSClassFromString(
|
|
856
|
+
"RNVoipPushNotificationManager") as? NSObject.Type
|
|
857
|
+
{
|
|
858
|
+
if RNVoipPushNotificationManager.responds(
|
|
859
|
+
to: Selector(("didUpdatePushCredentials:forType:")))
|
|
860
|
+
{
|
|
861
|
+
RNVoipPushNotificationManager.perform(
|
|
862
|
+
Selector(("didUpdatePushCredentials:forType:")),
|
|
863
|
+
with: pushCredentials,
|
|
864
|
+
with: type.rawValue
|
|
865
|
+
)
|
|
866
|
+
NSLog(
|
|
867
|
+
"[TelnyxVoipPushHandler] Forwarded token to RNVoipPushNotificationManager")
|
|
868
|
+
} else {
|
|
869
|
+
NSLog(
|
|
870
|
+
"[TelnyxVoipPushHandler] RNVoipPushNotificationManager doesn't respond to didUpdatePushCredentials"
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
NSLog("[TelnyxVoipPushHandler] RNVoipPushNotificationManager class not found")
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#endif
|