|
|
|
|
@ -1,13 +1,18 @@
|
|
|
|
|
import UIKit
|
|
|
|
|
import Flutter
|
|
|
|
|
import Firebase
|
|
|
|
|
import PushKit
|
|
|
|
|
import flutter_callkit_incoming
|
|
|
|
|
|
|
|
|
|
@main
|
|
|
|
|
@objc class AppDelegate: FlutterAppDelegate {
|
|
|
|
|
@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]?
|
|
|
|
|
@ -21,6 +26,12 @@ import Firebase
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -34,6 +45,130 @@ import Firebase
|
|
|
|
|
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],
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|