289 lines
8.3 KiB
Swift
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
|
|
}
|
|
}
|