import UIKit import Flutter import Firebase import PushKit import flutter_callkit_incoming @main @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate { // Expose the Flutter engine for SceneDelegate to use lazy var flutterEngine = FlutterEngine(name: "my flutter engine") // Must be retained for the lifetime of the app, otherwise VoIP pushes stop. private var voipRegistry: PKPushRegistry? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Initialize Firebase FirebaseApp.configure() // Start running the Flutter engine flutterEngine.run() // Register plugins with the engine GeneratedPluginRegistrant.register(with: self.flutterEngine) // Register for VoIP pushes AFTER plugins, so flutter_callkit_incoming is ready. let registry = PKPushRegistry(queue: .main) registry.delegate = self registry.desiredPushTypes = [.voIP] voipRegistry = registry return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // UIScene lifecycle support override func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { let sceneConfiguration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) sceneConfiguration.delegateClass = SceneDelegate.self return sceneConfiguration } // MARK: - PushKit (VoIP) 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() // Persisted by the plugin; read in Dart via FlutterCallkitIncoming.getDevicePushTokenVoIP(). SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(token) } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { guard type == .voIP else { return } SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("") } // iOS 13+: every VoIP push MUST be reported to CallKit before `completion` is called, // or iOS terminates the app and eventually stops delivering VoIP pushes to it. func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void ) { guard type == .voIP, let plugin = SwiftFlutterCallkitIncomingPlugin.sharedInstance else { completion() return } let data = flutter_callkit_incoming.Data(args: AppDelegate.callKitArgs(from: payload.dictionaryPayload)) plugin.showCallkitIncoming(data, fromPushKit: true) { completion() } } /// Builds the same structure Dart's `CallKitParams.toJson()` produces in /// `handleDataMessage` (firebase_notification_manger.dart), so both paths behave identically. /// Accepts the call fields at the payload root, or nested under "data"/"extra"/"payload" /// (as a dictionary or a JSON string). private static func callKitArgs(from raw: [AnyHashable: Any]) -> [String: Any?] { var p: [String: Any] = [:] for (k, v) in raw { if let key = k as? String { p[key] = v } } for key in ["data", "extra", "payload"] { if let nested = p[key] as? [String: Any] { p.merge(nested) { _, new in new } } else if let json = p[key] as? String, let d = json.data(using: .utf8), let nested = (try? JSONSerialization.jsonObject(with: d)) as? [String: Any] { p.merge(nested) { _, new in new } } } func str(_ keys: String...) -> String? { for k in keys { if let v = p[k], !(v is NSNull) { let s = "\(v)" if !s.isEmpty { return s } } } return nil } let callId = str("callId", "sessionId") ?? UUID().uuidString // CallKit requires a real UUID; the backend id is kept in `extra.callId`. let uuid = UUID(uuidString: callId)?.uuidString ?? UUID().uuidString let callerId = str("callerId", "callerEmployeeNumber", "sourceUserId") ?? "unknown" let callerName = str("callerName", "callerUserName", "userName") ?? "Unknown Caller" let callerEmployeeNumber = str("callerEmployeeNumber") ?? callerId let calleeEmployeeNumber = str("calleeEmployeeNumber") ?? callerId let isVideoCall: Bool = { if let b = p["isVideoCall"] as? Bool { return b } if let s = p["isVideoCall"] as? String { return s.lowercased() == "true" } return str("callType")?.lowercased() == "video" }() return [ "id": uuid, "nameCaller": callerName, "appName": "ATOMS", "avatar": "", "handle": callerEmployeeNumber, "type": isVideoCall ? 1 : 0, "duration": 30000, "extra": [ "callId": callId, "callerId": callerId, "callerEmployeeNumber": callerEmployeeNumber, "calleeEmployeeNumber": calleeEmployeeNumber, "callerName": callerName, "moduleId": str("moduleId", "applicationId") ?? "", "referenceId": str("referenceId") ?? "", "conversationId": str("conversationId") ?? "", "isVideoCall": isVideoCall, "timestamp": ISO8601DateFormatter().string(from: Date()), ] as [String: Any], "missedCallNotification": [ "showNotification": true, "isShowCallback": false, "subtitle": "Missed call", "callbackText": "Call back", ] as [String: Any], "callingNotification": [ "showNotification": true, "isShowCallback": true, "subtitle": "Calling...", "callbackText": "Hang Up", ] as [String: Any], "ios": [ "iconName": "CallKitLogo", "handleType": "generic", "supportsVideo": true, "maximumCallGroups": 1, "maximumCallsPerCallGroup": 1, "audioSessionMode": "default", "audioSessionActive": true, "audioSessionPreferredSampleRate": 44100.0, "audioSessionPreferredIOBufferDuration": 0.005, "supportsDTMF": true, "supportsHolding": false, "supportsGrouping": false, "supportsUngrouping": false, "ringtonePath": "system_ringtone_default", ] as [String: Any], ] } }