|
import ExpoModulesCore |
|
import Foundation |
|
import PushKit |
|
import CallKit |
|
|
|
class VoipPushDelegate: NSObject, PKPushRegistryDelegate, CXProviderDelegate { |
|
|
|
var onTokenReceived: ((String) -> Void)? |
|
var onIncomingPush: ((PKPushPayload) -> Void)? |
|
var onCallAccepted: ((UUID) -> Void)? |
|
var onCallEnded: ((UUID) -> Void)? |
|
|
|
var voipRegistrationToken: String? |
|
var pushRegistry: PKPushRegistry? |
|
|
|
private let callProvider: CXProvider |
|
private let callController = CXCallController() |
|
|
|
override init() { |
|
let config = CXProviderConfiguration(localizedName: "App Name") |
|
config.supportsVideo = true |
|
config.maximumCallsPerCallGroup = 1 |
|
config.supportedHandleTypes = [.generic] |
|
|
|
pushRegistry = PKPushRegistry(queue: DispatchQueue.main) |
|
|
|
callProvider = CXProvider(configuration: config) |
|
|
|
super.init() |
|
|
|
pushRegistry?.delegate = self |
|
pushRegistry?.desiredPushTypes = [.voIP] |
|
|
|
|
|
callProvider.setDelegate(self, queue: nil) |
|
|
|
} |
|
|
|
func pushRegistry( |
|
_ registry: PKPushRegistry, |
|
didUpdate pushCredentials: PKPushCredentials, |
|
for type: PKPushType |
|
) { |
|
guard type == .voIP else { return } |
|
|
|
let tokenData = pushCredentials.token |
|
let tokenParts = tokenData.map { String(format: "%02x", $0) } |
|
voipRegistrationToken = tokenParts.joined() |
|
|
|
onTokenReceived?(voipRegistrationToken ?? "") |
|
} |
|
|
|
func pushRegistry( |
|
_ registry: PKPushRegistry, |
|
didReceiveIncomingPushWith payload: PKPushPayload, |
|
for type: PKPushType, |
|
completion: @escaping () -> Void |
|
) { |
|
guard type == .voIP else { |
|
completion() |
|
return |
|
} |
|
|
|
// CallKit expects us to call this immediately |
|
let update = CXCallUpdate() |
|
update.hasVideo = true |
|
update.remoteHandle = CXHandle( |
|
type: .generic, |
|
value: (payload.dictionaryPayload["callerName"] as? String ?? "Unknown Caller") |
|
) |
|
|
|
let uuid = payload.dictionaryPayload["uuid"] as? String |
|
|
|
callProvider.reportNewIncomingCall(with: UUID(uuidString: uuid ?? "") ?? UUID(), update: update) { error in |
|
if let error = error { |
|
print("Error reporting call: \(error)") |
|
} else { |
|
self.onIncomingPush?(payload) |
|
} |
|
completion() |
|
} |
|
} |
|
|
|
func startCall(callUUID: String, handle: String, callerName: String) { |
|
let callUUIDObj = UUID(uuidString: callUUID) ?? UUID() |
|
let handle = CXHandle(type: .generic, value: handle) |
|
|
|
let callAction = CXStartCallAction(call: callUUIDObj, handle: handle) |
|
callAction.isVideo = true |
|
|
|
let transaction = CXTransaction(action: callAction) |
|
self.callController.request(transaction, completion: { error in}) |
|
} |
|
|
|
func endCall(callUUID: String) { |
|
let callUUIDObj = UUID(uuidString: callUUID) ?? UUID() |
|
let action = CXEndCallAction(call: callUUIDObj) |
|
|
|
let transaction = CXTransaction(action: action) |
|
self.callController.request(transaction, completion: { error in}) |
|
} |
|
|
|
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { |
|
// Handle the action, e.g., start audio, setup media, etc. |
|
let callUUID = action.callUUID |
|
self.onCallAccepted?(callUUID) |
|
action.fulfill() |
|
} |
|
|
|
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { |
|
let callUUID = action.callUUID |
|
self.onCallEnded?(callUUID) |
|
action.fulfill() |
|
} |
|
|
|
func providerDidReset(_ provider: CXProvider) { |
|
// Handle provider reset if needed |
|
} |
|
} |
|
|
|
public final class ExpoVoipPushTokenModule: Module { |
|
private let delegate = VoipPushDelegate() |
|
|
|
private func initializePushDelegate() { |
|
|
|
delegate.onTokenReceived = { [weak self] token in |
|
self?.sendEvent("onRegistration", ["voipToken": token]) |
|
} |
|
|
|
delegate.onIncomingPush = { [weak self] payload in |
|
let payloadDict = payload.dictionaryPayload |
|
|
|
self?.sendEvent("notification", ["payload": payloadDict]) |
|
} |
|
|
|
delegate.onCallAccepted = { [weak self] uuid in |
|
self?.sendEvent("onCallAccepted", ["callUUID": uuid.uuidString]) |
|
} |
|
|
|
delegate.onCallEnded = { [weak self] uuid in |
|
self?.sendEvent("onCallEnded", ["callUUID": uuid.uuidString]) |
|
} |
|
} |
|
|
|
private func startCall(callUUID: String, handle: String, callerName: String) { |
|
delegate.startCall(callUUID: callUUID, handle: handle, callerName: callerName) |
|
} |
|
|
|
private func endCall(callUUID: String) { |
|
delegate.endCall(callUUID: callUUID) |
|
} |
|
|
|
public func definition() -> ModuleDefinition { |
|
|
|
Name("ExpoVoipPushToken") |
|
|
|
Events("onRegistration", "notification", "onCallAccepted", "onCallEnded") |
|
|
|
OnCreate(){ |
|
self.initializePushDelegate() |
|
} |
|
|
|
Function("startCall") { (callUUID: String, handle: String, callerName: String) -> Void in |
|
self.startCall(callUUID: callUUID, handle: handle, callerName: callerName) |
|
} |
|
|
|
Function("endCall") { (callUUID: String) -> Void in |
|
self.endCall(callUUID: callUUID) |
|
} |
|
|
|
Function("registerVoipPushToken") { |
|
self.sendEvent("onRegistration", ["voipToken": self.delegate.voipRegistrationToken ?? ""]) |
|
} |
|
} |
|
} |
We don't need to rewrite the call logic with CallKit here because we already have CallKeep, which handles this part well.
You can reuse CallKeep’s functionality by adding the dependency to your .podspec file like this:
s.dependency 'RNCallKeep'Then, simply import it into your Swift file and use it as needed.
For more details, please refer to the images below.