import Foundation
import Expo
import FirebaseCore
import PushKit
import React
import ReactAppDependencyProvider
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
private var voipRegistry: PKPushRegistry?
private var voipTokenHandler: ((String) -> Void)?
private var voipPayloadHandler: (([String: Any]) -> Void)?
private let voipTokenStorageKey = "voip_push_token"
private var voipToken: String?
private var callKeepConfigured = false
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a
FirebaseApp.configure()
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
setupCallKeepIfNeeded()
startVoipPushRegistry()
return result
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY)
if url.host?.lowercased() == "firebaseauth" {
// invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router
return false
}
// @generated end @react-native-firebase/auth-openURL
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
func startVoipPushRegistry() {
if voipRegistry != nil { return }
print("[VoipPush] startVoipPushRegistry")
let registry = PKPushRegistry(queue: DispatchQueue.main)
registry.delegate = self
registry.desiredPushTypes = [.voIP]
voipRegistry = registry
}
func setVoipTokenHandler(_ handler: @escaping (String) -> Void) {
voipTokenHandler = handler
}
func setVoipPayloadHandler(_ handler: @escaping ([String: Any]) -> Void) {
voipPayloadHandler = handler
}
func stopVoipPushRegistry(clearToken: Bool = false) {
print("[VoipPush] stopVoipPushRegistry clearToken=\(clearToken)")
voipRegistry?.desiredPushTypes = []
voipRegistry?.delegate = nil
voipRegistry = nil
voipTokenHandler = nil
if clearToken { clearVoipToken() }
}
func getVoipToken() -> String? {
return voipToken ?? UserDefaults.standard.string(forKey: voipTokenStorageKey)
}
private func storeVoipToken(_ token: String) {
voipToken = token
UserDefaults.standard.set(token, forKey: voipTokenStorageKey)
}
private func clearVoipToken() {
voipToken = nil
UserDefaults.standard.removeObject(forKey: voipTokenStorageKey)
}
private func setupCallKeepIfNeeded() {
if callKeepConfigured { return }
callKeepConfigured = true
let appName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String)
?? (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String)
?? "Deafiline"
RNCallKeep.setup([
"appName": appName,
])
print("[CallKeep] native setup done (\(appName))")
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
extension AppDelegate: PKPushRegistryDelegate {
public func pushRegistry(
_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType
) {
guard type == .voIP else { return }
let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
print("VoIP token: \(token)")
storeVoipToken(token)
voipTokenHandler?(token)
}
public func pushRegistry(
_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType
) {
guard type == .voIP else { return }
print("VoIP token invalidated")
clearVoipToken()
}
public func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
guard type == .voIP else {
completion()
return
}
let payloadDict = payload.dictionaryPayload
if let handler = voipPayloadHandler {
handler(payloadDict as? [String: Any] ?? [:])
}
let callUUID = (payloadDict["callUUID"] as? String) ?? UUID().uuidString.lowercased()
let handle = (payloadDict["handle"] as? String) ?? "Unknown"
let callerName = (payloadDict["callerName"] as? String) ?? "Incoming Call"
let hasVideo = (payloadDict["hasVideo"] as? Bool) ?? false
RNCallKeep.reportNewIncomingCall(
callUUID,
handle: handle,
handleType: "generic",
hasVideo: hasVideo,
localizedCallerName: callerName,
supportsHolding: true,
supportsDTMF: true,
supportsGrouping: false,
supportsUngrouping: false,
fromPushKit: true,
payload: payloadDict,
withCompletionHandler: completion
)
}
}