import Foundation import UserNotifications import PushKit import CallKit @objc(AeThexConnectModule) class AeThexConnectModule: NSObject { private var callKitProvider: CXProvider? private var callKitController: CXCallController? private var voipRegistry: PKPushRegistry? @objc static func requiresMainQueueSetup() -> Bool { return true } // MARK: - VoIP Push Notifications @objc func registerForVoIPPushes(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry?.delegate = self voipRegistry?.desiredPushTypes = [.voIP] resolver(true) } // MARK: - CallKit Integration @objc func initializeCallKit() { let configuration = CXProviderConfiguration(localizedName: "AeThex Connect") configuration.supportsVideo = true configuration.maximumCallGroups = 1 configuration.maximumCallsPerCallGroup = 1 configuration.supportedHandleTypes = [.generic] configuration.iconTemplateImageData = UIImage(named: "CallKitIcon")?.pngData() callKitProvider = CXProvider(configuration: configuration) callKitProvider?.setDelegate(self, queue: nil) callKitController = CXCallController() } @objc func reportIncomingCall(_ callId: String, callerName: String, hasVideo: Bool, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { guard let provider = callKitProvider else { rejecter("NO_PROVIDER", "CallKit provider not initialized", nil) return } let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .generic, value: callerName) update.hasVideo = hasVideo update.localizedCallerName = callerName let uuid = UUID(uuidString: callId) ?? UUID() provider.reportNewIncomingCall(with: uuid, update: update) { error in if let error = error { rejecter("CALL_ERROR", error.localizedDescription, error) } else { resolver(true) } } } @objc func startCall(_ callId: String, recipientName: String, hasVideo: Bool, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { guard let controller = callKitController else { rejecter("NO_CONTROLLER", "CallKit controller not initialized", nil) return } let uuid = UUID(uuidString: callId) ?? UUID() let handle = CXHandle(type: .generic, value: recipientName) let startCallAction = CXStartCallAction(call: uuid, handle: handle) startCallAction.isVideo = hasVideo let transaction = CXTransaction(action: startCallAction) controller.request(transaction) { error in if let error = error { rejecter("CALL_ERROR", error.localizedDescription, error) } else { resolver(true) } } } @objc func endCall(_ callId: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { guard let controller = callKitController else { rejecter("NO_CONTROLLER", "CallKit controller not initialized", nil) return } let uuid = UUID(uuidString: callId) ?? UUID() let endCallAction = CXEndCallAction(call: uuid) let transaction = CXTransaction(action: endCallAction) controller.request(transaction) { error in if let error = error { rejecter("CALL_ERROR", error.localizedDescription, error) } else { resolver(true) } } } // MARK: - Background Voice Chat @objc func startBackgroundVoice(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { do { let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .defaultToSpeaker]) try audioSession.setActive(true) resolver(true) } catch { rejecter("AUDIO_ERROR", error.localizedDescription, error) } } @objc func stopBackgroundVoice(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { do { let audioSession = AVAudioSession.sharedInstance() try audioSession.setActive(false) resolver(true) } catch { rejecter("AUDIO_ERROR", error.localizedDescription, error) } } } // MARK: - PKPushRegistryDelegate extension AeThexConnectModule: PKPushRegistryDelegate { func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined() // Send token to React Native NotificationCenter.default.post( name: NSNotification.Name("VoIPTokenReceived"), object: nil, userInfo: ["token": token] ) } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { guard type == .voIP else { completion() return } // Extract call info from payload let callId = payload.dictionaryPayload["callId"] as? String ?? UUID().uuidString let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown" let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool ?? false // Report to CallKit reportIncomingCall(callId, callerName: callerName, hasVideo: hasVideo, resolver: { _ in completion() }, rejecter: { _, _, _ in completion() }) } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { // Token invalidated print("VoIP token invalidated") } } // MARK: - CXProviderDelegate extension AeThexConnectModule: CXProviderDelegate { func providerDidReset(_ provider: CXProvider) { // End all calls NotificationCenter.default.post( name: NSNotification.Name("CallsReset"), object: nil ) } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { // Answer call NotificationCenter.default.post( name: NSNotification.Name("AnswerCall"), object: nil, userInfo: ["callId": action.callUUID.uuidString] ) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { // End call NotificationCenter.default.post( name: NSNotification.Name("EndCall"), object: nil, userInfo: ["callId": action.callUUID.uuidString] ) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { // Mute/unmute NotificationCenter.default.post( name: NSNotification.Name("SetMuted"), object: nil, userInfo: [ "callId": action.callUUID.uuidString, "muted": action.isMuted ] ) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { // Hold/unhold NotificationCenter.default.post( name: NSNotification.Name("SetHeld"), object: nil, userInfo: [ "callId": action.callUUID.uuidString, "held": action.isOnHold ] ) action.fulfill() } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { // Audio session activated NotificationCenter.default.post( name: NSNotification.Name("AudioSessionActivated"), object: nil ) } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { // Audio session deactivated NotificationCenter.default.post( name: NSNotification.Name("AudioSessionDeactivated"), object: nil ) } } // MARK: - React Native Bridge @objc(AeThexConnectModuleBridge) class AeThexConnectModuleBridge: NSObject { @objc static func requiresMainQueueSetup() -> Bool { return true } }