AeThex-Connect/packages/mobile/ios/AeThexConnectModule.swift
2026-01-10 08:00:59 +00:00

289 lines
8.3 KiB
Swift

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
}
}