@telnyx/react-voice-commons-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/TelnyxVoiceCommons.podspec +32 -0
  2. package/ios/CallKitBridge.m +44 -0
  3. package/ios/CallKitBridge.swift +879 -0
  4. package/ios/README.md +211 -0
  5. package/ios/VoicePnBridge.m +31 -0
  6. package/ios/VoicePnBridge.swift +87 -0
  7. package/lib/callkit/callkit-coordinator.d.ts +126 -0
  8. package/lib/callkit/callkit-coordinator.js +728 -0
  9. package/lib/callkit/callkit.d.ts +49 -0
  10. package/lib/callkit/callkit.js +262 -0
  11. package/lib/callkit/index.d.ts +4 -0
  12. package/lib/callkit/index.js +15 -0
  13. package/lib/callkit/use-callkit-coordinator.d.ts +21 -0
  14. package/lib/callkit/use-callkit-coordinator.js +53 -0
  15. package/lib/callkit/use-callkit.d.ts +28 -0
  16. package/lib/callkit/use-callkit.js +279 -0
  17. package/lib/context/TelnyxVoiceContext.d.ts +18 -0
  18. package/lib/context/TelnyxVoiceContext.js +18 -0
  19. package/lib/hooks/use-callkit-coordinator.d.ts +13 -0
  20. package/lib/hooks/use-callkit-coordinator.js +48 -0
  21. package/lib/hooks/useAppReadyNotifier.d.ts +9 -0
  22. package/lib/hooks/useAppReadyNotifier.js +25 -0
  23. package/lib/hooks/useAppStateHandler.d.ts +16 -0
  24. package/lib/hooks/useAppStateHandler.js +105 -0
  25. package/lib/index.d.ts +24 -0
  26. package/lib/index.js +66 -0
  27. package/lib/internal/CallKitHandler.d.ts +17 -0
  28. package/lib/internal/CallKitHandler.js +110 -0
  29. package/lib/internal/callkit-manager.d.ts +69 -0
  30. package/lib/internal/callkit-manager.js +326 -0
  31. package/lib/internal/calls/call-state-controller.d.ts +92 -0
  32. package/lib/internal/calls/call-state-controller.js +294 -0
  33. package/lib/internal/session/session-manager.d.ts +87 -0
  34. package/lib/internal/session/session-manager.js +385 -0
  35. package/lib/internal/user-defaults-helpers.d.ts +10 -0
  36. package/lib/internal/user-defaults-helpers.js +69 -0
  37. package/lib/internal/voice-pn-bridge.d.ts +14 -0
  38. package/lib/internal/voice-pn-bridge.js +5 -0
  39. package/lib/models/call-state.d.ts +61 -0
  40. package/lib/models/call-state.js +87 -0
  41. package/lib/models/call.d.ts +145 -0
  42. package/lib/models/call.js +372 -0
  43. package/lib/models/config.d.ts +64 -0
  44. package/lib/models/config.js +92 -0
  45. package/lib/models/connection-state.d.ts +34 -0
  46. package/lib/models/connection-state.js +50 -0
  47. package/lib/telnyx-voice-app.d.ts +48 -0
  48. package/lib/telnyx-voice-app.js +486 -0
  49. package/lib/telnyx-voip-client.d.ts +184 -0
  50. package/lib/telnyx-voip-client.js +386 -0
  51. package/package.json +104 -0
  52. package/src/callkit/callkit-coordinator.ts +846 -0
  53. package/src/callkit/callkit.ts +322 -0
  54. package/src/callkit/index.ts +4 -0
  55. package/src/callkit/use-callkit.ts +345 -0
  56. package/src/context/TelnyxVoiceContext.tsx +33 -0
  57. package/src/hooks/use-callkit-coordinator.ts +60 -0
  58. package/src/hooks/useAppReadyNotifier.ts +25 -0
  59. package/src/hooks/useAppStateHandler.ts +134 -0
  60. package/src/index.ts +56 -0
  61. package/src/internal/CallKitHandler.tsx +149 -0
  62. package/src/internal/callkit-manager.ts +335 -0
  63. package/src/internal/calls/call-state-controller.ts +384 -0
  64. package/src/internal/session/session-manager.ts +467 -0
  65. package/src/internal/user-defaults-helpers.ts +58 -0
  66. package/src/internal/voice-pn-bridge.ts +18 -0
  67. package/src/models/call-state.ts +98 -0
  68. package/src/models/call.ts +388 -0
  69. package/src/models/config.ts +125 -0
  70. package/src/models/connection-state.ts +50 -0
  71. package/src/telnyx-voice-app.tsx +690 -0
  72. package/src/telnyx-voip-client.ts +475 -0
  73. package/src/types/telnyx-sdk.d.ts +79 -0
@@ -0,0 +1,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