hiennguyen92 / flutter_callkit_incoming

Flutter Callkit Incoming
https://pub.dev/packages/flutter_callkit_incoming
MIT License
180 stars 312 forks source link

Error receiving PushVoIP in the background IOS #583

Open robsonvasquez opened 2 months ago

robsonvasquez commented 2 months ago

I receive the PushVoIP notification normally on iOS when the app is in the foreground, but when it goes to the background or is terminated, I don’t receive the PushVoIP notification.

delegate:

import UIKit import Flutter

import awesome_notifications //import shared_preferences_ios import shared_preferences_foundation

import CallKit import AVFAudio import PushKit import flutter_callkit_incoming import WebRTC

@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate { //var flutterEngine: FlutterEngine?

override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool {

if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)

SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in          
      SwiftAwesomeNotificationsPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!)          
      SharedPreferencesPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
  }

//Setup VOIP
let mainQueue = DispatchQueue.main
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]

//Use if using WebRTC
RTCAudioSession.sharedInstance().useManualAudio = true
RTCAudioSession.sharedInstance().isAudioEnabled = false

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

}

// Call back from Recent history override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    guard let handleObj = userActivity.handle else {
        return false
    }

    guard let isVideo = userActivity.isVideo else {
        return false
    }
    let objData = handleObj.getDecryptHandle()
    let nameCaller = objData["nameCaller"] as? String ?? ""
    let handle = objData["handle"] as? String ?? ""
    let data = flutter_callkit_incoming.Data(id: UUID().uuidString, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
    //set more data...
    //data.nameCaller = nameCaller
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.startCall(data, fromPushKit: true)

    return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

// Handle updated push credentials
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
    print(credentials.token)
    let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
    print(deviceToken)
    //Save deviceToken to your server
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
    print("didInvalidatePushTokenFor")
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}

// Handle incoming pushes
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    print("didReceiveIncomingPushWith t")
    print(payload.dictionaryPayload)
    guard type == .voIP else { 
        return print("aqui")
    }

    // let id = payload.dictionaryPayload["id"] as? String ?? ""
    // let nameCaller = payload.dictionaryPayload["nameCaller"] as? String ?? ""
    // let handle = payload.dictionaryPayload["handle"] as? String ?? ""
    // let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false

    // let data = flutter_callkit_incoming.Data(id: id, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
    // //set more data
    // data.extra = ["user": "abc@123", "platform": "ios"]
    // //data.iconName = ...
    // //data.....
    // SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)

    // //Make sure call completion()
    // DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    //     completion()
    // }

    let domain = payload.dictionaryPayload["domain"] as? String ?? ""
    let iceTimeout = payload.dictionaryPayload["iceTimeout"] as? String ?? ""
    let ring_time = payload.dictionaryPayload["ring_time"] as? String ?? ""
    let stun = payload.dictionaryPayload["stun"] as? String ?? ""
    let stunURL = payload.dictionaryPayload["stunURL"] as? String ?? ""
    let tcpPort = payload.dictionaryPayload["tcpPort"] as? String ?? ""
    let turn = payload.dictionaryPayload["turn"] as? String ?? ""
    let turnCredential = payload.dictionaryPayload["turnCredential"] as? String ?? ""
    let turnURL = payload.dictionaryPayload["turnURL"] as? String ?? ""
    let turnUsername = payload.dictionaryPayload["turnUsername"] as? String ?? ""
    let wsdomain = payload.dictionaryPayload["wsdomain"] as? String ?? ""
    let sip_username = payload.dictionaryPayload["sip_username"] as? String ?? ""
    let sip_password = payload.dictionaryPayload["sip_password"] as? String ?? ""
    let zona = payload.dictionaryPayload["zona"] as? String ?? ""
    let zone_id = payload.dictionaryPayload["zone_id"] as? String ?? ""
    let conta = payload.dictionaryPayload["conta"] as? String ?? ""
    let nameAccount = payload.dictionaryPayload["nameAccount"] as? String ?? ""

    // Cria um dicionário com todos os valores
    let arguments: [String: Any] = [
        "domain": domain,
        "iceTimeout": iceTimeout,
        "ring_time": ring_time,
        "stun": stun,
        "stunURL": stunURL,
        "tcpPort": tcpPort,
        "turn": turn,
        "turnCredential": turnCredential,
        "turnURL": turnURL,
        "turnUsername": turnUsername,
        "wsdomain": wsdomain,
        "sip_username": sip_username,
        "sip_password": sip_password,
        "zona": zona,
        "zone_id": zone_id,
        "conta": conta,
        "nameAccount": nameAccount
    ]

    // Chamar método Flutter aqui
    callFlutterMethod(method: "startSip", arguments: arguments)
}

// Func Call api for Accept
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
    let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any]
    print("LOG: onAccept")
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data: \(data)")
            //Make sure call action.fulfill() when you are done(connected WebRTC - Start counting seconds)
            action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }
}

// Func Call API for Decline
func onDecline(_ call: Call, _ action: CXEndCallAction) {
    let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any]
    print("LOG: onDecline")
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data: \(data)")
            //Make sure call action.fulfill() when you are done
            action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }
}

// Func Call API for End
func onEnd(_ call: Call, _ action: CXEndCallAction) {
    let json = ["action": "END", "data": call.data.toJSON()] as [String: Any]
    print("LOG: onEnd")
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data: \(data)")
            //Make sure call action.fulfill() when you are done
            action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }
}

// Func Call API for TimeOut
func onTimeOut(_ call: Call) {
    let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any]
    print("LOG: onTimeOut")
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data: \(data)")

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }
}

// Func Callback Toggle Audio Session
func didActivateAudioSession(_ audioSession: AVAudioSession) {
    //Use if using WebRTC
    //RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
    //RTCAudioSession.sharedInstance().isAudioEnabled = true
}

// Func Callback Toggle Audio Session
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {
    //Use if using WebRTC
    //RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
    //RTCAudioSession.sharedInstance().isAudioEnabled = false
}

func performRequest(parameters: [String: Any], completion: @escaping (Result<Any, Error>) -> Void) {
    if let url = URL(string: "https://webhook.site/e32a591f-0d17-469d-a70d-33e9f9d60727") {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        //Add header

        do {
            let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])
            request.httpBody = jsonData
        } catch {
            completion(.failure(error))
            return
        }

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Empty data"])))
                return
            }

            do {
                let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
                completion(.success(jsonObject))
            } catch {
                completion(.failure(error))
            }
        }
        task.resume()
    } else {
        completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
    }
}

// Função para chamar o método Flutter
func callFlutterMethod(method: String, arguments: [String: Any]) {
    // guard let flutterEngine = self.flutterEngine,
    // let flutterViewController = flutterEngine.viewController else {
    //     print("Flutter engine not initialized.")
    //     return
    // }

    print(method)

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

    let channel = FlutterMethodChannel(name: "flutter_callkit_channel_ios", binaryMessenger: controller.binaryMessenger)

    channel.invokeMethod(method, arguments: arguments) { result in
        print("Result from Flutter: \(String(describing: result))")
    }
}

}

Essenbay commented 1 month ago

Same issue

pasanediri97 commented 1 month ago

same issue. any solutions?

Essenbay commented 1 month ago

Only in terminated and background right? "Apparently if you didn't have everything configured right, Apple will start to suspend your voip notifications and won't deliver." Seems I handled some events wrong (while firsly configuring my app) and apple suspended my bundle. Try to delete app and install again. Worked for me, spent whole day to figure this out.

robsonvasquez commented 1 month ago

That was exactly my mistake. I ended up removing and reinstalling, and it worked!

But now my problem is accepting the incoming call. I’m receiving it, but when I answer, it throws an error.

Essenbay commented 1 month ago

What error? Maybe you are passing not UUID value for id? Check https://github.com/hiennguyen92/flutter_callkit_incoming/issues/571#issuecomment-2283511374

Also, make sure you configure pushRegistry for your notification type and requirements. Here’s an example of how mine is set up:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    os_log("didReceiveIncomingPushWith", log: OSLog.default, type: .info)
    os_log("Payload received: %@", log: OSLog.default, type: .info, payload.dictionaryPayload.description)

    guard type == .voIP else { return }

    if let data = payload.dictionaryPayload["data"] as? [String: Any] {
        let uniqueID = UUID().uuidString
        let id = data["id"] as? String ?? ""
        let nameCaller = data["caller_name"] as? String ?? ""
        let handle = data["handle"] as? String ?? ""
        let isVideo = data["isVideo"] as? Bool ?? false

        os_log("ID: %{public}@", log: OSLog.default, type: .info, id)
        os_log("Caller Name: %{public}@", log: OSLog.default, type: .info, nameCaller)
        os_log("Handle: %{public}@", log: OSLog.default, type: .info, handle)
        os_log("Is Video: %d", log: OSLog.default, type: .info, isVideo ? 1 : 0)

        let callData = flutter_callkit_incoming.Data(id: uniqueID, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
        // Set additional data if necessary
        callData.extra = ["user": "abc@123", "platform": "ios", "caller_id": id, "nameCaller": nameCaller]

        // Show incoming call using CallKit
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(callData, fromPushKit: true)
    } else {
        os_log("Failed to parse data from payload")
    }

    // Make sure to call completion()
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion()
    }
}

Make sure the id you're passing is in UUID format, and confirm the payload structure matches what is expected.

robsonvasquez commented 1 month ago

Currently, I’m receiving the VoIP notification. Upon receiving it, I register with my SIP server, and after registering and receiving the Invite, I want to display the notification that a call is incoming. However, it seems that I’m forced to show this notification in the function that handles receiving the VoIP notification, even though I haven’t received the call Invite yet. I want to register upon receiving the VoIP notification and only generate the incoming call notification after receiving the Invite, but it’s not working.

// Handle incoming pushes func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { print("didReceiveIncomingPushWith t") print(payload.dictionaryPayload) print(type) guard type == .voIP else {return}

    // let id = payload.dictionaryPayload["uuid"] as? String ?? ""
    // let nameCaller = payload.dictionaryPayload["callerName"] as? String ?? ""
    // let handle = payload.dictionaryPayload["handle"] as? String ?? ""
    // let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false

    // let data = flutter_callkit_incoming.Data(id: id, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
    // //set more data
    // data.extra = ["user": "abc@123", "platform": "ios"]
    // //data.iconName = ...
    // //data.....
    //SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)

    let domain = payload.dictionaryPayload["domain"] as? String ?? ""
    let iceTimeout = payload.dictionaryPayload["iceTimeout"] as? String ?? ""
    let ring_time = payload.dictionaryPayload["ring_time"] as? String ?? ""
    let stun = payload.dictionaryPayload["stun"] as? String ?? ""
    let stunURL = payload.dictionaryPayload["stunURL"] as? String ?? ""
    let tcpPort = payload.dictionaryPayload["tcpPort"] as? String ?? ""
    let turn = payload.dictionaryPayload["turn"] as? String ?? ""
    let turnCredential = payload.dictionaryPayload["turnCredential"] as? String ?? ""
    let turnURL = payload.dictionaryPayload["turnURL"] as? String ?? ""
    let turnUsername = payload.dictionaryPayload["turnUsername"] as? String ?? ""
    let wsdomain = payload.dictionaryPayload["wsdomain"] as? String ?? ""
    let sip_username = payload.dictionaryPayload["sip_username"] as? String ?? ""
    let sip_password = payload.dictionaryPayload["sip_password"] as? String ?? ""
    let zona = payload.dictionaryPayload["zona"] as? String ?? "teste"
    let zone_id = payload.dictionaryPayload["zone_id"] as? String ?? ""
    let conta = payload.dictionaryPayload["conta"] as? String ?? ""
    let nameAccount = payload.dictionaryPayload["nameAccount"] as? String ?? ""

    // Cria um dicionário com todos os valores
    let arguments: [String: Any] = [
        "domain": domain,
        "iceTimeout": iceTimeout,
        "ring_time": ring_time,
        "stun": stun,
        "stunURL": stunURL,
        "tcpPort": tcpPort,
        "turn": turn,
        "turnCredential": turnCredential,
        "turnURL": turnURL,
        "turnUsername": turnUsername,
        "wsdomain": wsdomain,
        "sip_username": sip_username,
        "sip_password": sip_password,
        "zona": zona,
        "zone_id": zone_id,
        "conta": conta,
        "nameAccount": nameAccount
    ]

    // Chamar método Flutter aqui
    callFlutterMethod(method: "startSip", arguments: arguments)

    //Make sure call completion()
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}