https://d226lax1qjow5r.cloudfront.net/blog/blogposts/handling-voip-push-notifications-with-callkit/callkit-push-notifications.png

Cómo manejar las notificaciones push de VoIP con iOS Callkit

Publicado el May 25, 2023

Tiempo de lectura: 8 minutos

En este tutorial, utilizará CallKit para manejar las notificaciones push de VoIP enviadas a un dispositivo iOS al usar el Vonage Client SDK para iOS. Las notificaciones push de VoIP son el método principal para recibir llamadas entrantes con el Client SDK, ya que llegarán al dispositivo independientemente de si tu aplicación está en primer plano o no. CallKit te permite integrar tu aplicación iOS en el sistema para que tu aplicación se vea como una llamada telefónica nativa de iOS. Usar el Client SDK de Vonage con Callkit te permitirá incorporar llamadas en tu aplicación y, al mismo tiempo, tener una interfaz consistente y familiar para las llamadas entrantes.

Requisitos previos

  • Una Account API de Vonage. Si aún no tienes una, puedes inscribirte hoy.

  • Una Apple Developer Account y un dispositivo iOS (o simulador en Xcode 14 y versiones superiores).

  • Account en GitHub.

  • Xcode 12 y Swift 5 o superior.

  • Cocoapods para instalar el Client SDK de Vonage para iOS.

  • Nuestra interfaz de línea de comandos. Puede instalarlo con npm install -g @vonage/cli.

El proyecto Starter

Este blog se construirá sobre el "Recibir una llamada telefónica en la aplicación" del portal para desarrolladores de Vonage. Este tutorial comenzará con el estado final del proyecto del tutorial. Puedes seguir el tutorial o, si ya estás familiarizado con la creación de una aplicación de voz de Vonage Client SDK, puedes clonar el proyecto de inicio de GitHub.

Establecer certificados push

Hay dos tipos de notificaciones push que puedes usar en una aplicación iOS, VoIP pushes con PushKit o Notificaciones de Usuario. Este tutorial se centrará en las notificaciones push VoIP. El servicio de Notificaciones Push de Apple (APNs) utiliza autenticación basada en certificados para asegurar las conexiones entre APNs y los servidores de Vonage. Así que necesitarás crear un certificado y subirlo a los servidores de Vonage para que Vonage pueda enviar un push al dispositivo cuando haya una llamada entrante.

Añadir una función de notificación push

Para utilizar las notificaciones push, es necesario añadir la capacidad de notificaciones push a su proyecto Xcode. Asegúrese de que ha iniciado sesión en su cuenta de desarrollador de Apple en Xcode a través de las preferencias. Si es así, seleccione su objetivo y luego elija Firma y capacidades:

signing and capabilities tag

A continuación, seleccione añadir capacidad y añada Notificaciones Push y Modos de fondo y Modos de fondo:

add capability button

En la opción Modos de fondo, seleccione Voz sobre IP y Proceso de fondo. Si Xcode gestiona automáticamente la firma de su aplicación, actualizará el perfil de aprovisionamiento vinculado a su identificador de paquete para incluir las capacidades.

Cuando utilices notificaciones push de VoIP, tendrás que usar el framework CallKit. Enlázalo a tu proyecto añadiéndolo en Frameworks, bibliotecas y contenido integrado en General:

add callkit framework

Generación de un certificado push

Para generar un certificado push, deberá iniciar sesión en su cuenta de desarrollador de Apple y dirigirse a la sección Certificados, identificadores y perfiles y añadir un nuevo certificado:

add certificate button

Elija Servicio Apple Push Notification SSL (Sandbox & Producción) y continúe.

Certificate wizard checkbox

Ahora tendrá que elegir el ID de la aplicación a la que desea añadir notificaciones push de VoIP y continuar. Si tu aplicación no aparece en la lista, tendrás que crear un App ID. Xcode puede hacerlo por usted si gestiona automáticamente su firma. De lo contrario, puede crear un nuevo App ID en la sección Certificados, identificadores y perfiles en Identificadores. Asegúrese de seleccionar la función de notificaciones push al hacerlo.

Se le pedirá que cargue una solicitud de firma de certificado (CSR). Puede seguir las instrucciones en sitio web de ayuda de Apple para crear una CSR en tu Mac. Una vez cargado el CSR, podrá descargar el certificado. Haga doble clic en el archivo .cer para instalarlo en Acceso a Llaveros.

Para obtener el certificado push en el formato que necesitan los servidores de Vonage, deberás exportarlo. Localiza tu certificado de Servicios VoIP en Acceso a llaveros y haz clic con el botón derecho para exportarlo. Asigna un nombre a la exportación applecert y selecciona .p12 como formato:

keychain access export

Cargue su certificado Push

Usted subir su certificado certificado a Vonage usando el panel de control de la API. Abre tu aplicación en el panely luego abre la opción "Habilitar push notificaciones" :

Push Upload on the dashboardPush Upload on the dashboard

Puede cargar su certificado .p12 del paso anterior y añadir una contraseña si es necesario.

La clase ClientManager

Cree un nuevo archivo Swift (CMD + N) y llámalo ClientManager. Esta clase encapsulará el código necesario para interactuar con el Client SDK ya que necesitarás obtener información del Client SDK en múltiples lugares en futuros pasos:

Sustituya ALICE_JWT con el JWT que generó anteriormente, en un entorno de producción aquí es donde usted obtendría un JWT de su servidor de autenticación / punto final.

Con esta nueva clase, tendrá que mover el código de la llamada Client SDK de la clase ViewController a la clase ClientManager a la clase Las dos clases se comunicarán con los ClientManagerDelegate observadores. Realice los siguientes cambios en su ViewController clase:

class ViewController: UIViewController {
    
    private let connectionStatusLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        ClientManager.shared.delegate = self
        
        connectionStatusLabel.text = ""
        connectionStatusLabel.textAlignment = .center
        connectionStatusLabel.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(connectionStatusLabel)
        
        view.addConstraints([
            connectionStatusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            connectionStatusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
}

extension ViewController: ClientManagerDelegate {
    func clientStatusUpdated(_ clientManager: ClientManager, status: String) {
        DispatchQueue.main.async {
            self.connectionStatusLabel.text = status
        }
    }
}

Regístrese para recibir notificaciones push

El siguiente paso es registrar un dispositivo para notificaciones push a fin de que Vonage sepa a qué dispositivo enviar la notificación push para cada usuario. En la clase ClientManager agrega la propiedad pushToken y las siguientes funciones para manejar el token push del dispositivo:

final class ClientManager: NSObject {
    public var pushToken: Data?
    weak var delegate: ClientManagerDelegate?
    ...

    func invalidatePushToken(_ completion: (() -> Void)? = nil) {
        print("VPush: Invalidate token")
        if let deviceId = UserDefaults.standard.object(forKey: Constants.deviceId) as? String {
            client.unregisterDeviceTokens(byDeviceId: deviceId) { error in
                if error == nil {
                    self.pushToken = nil
                    UserDefaults.standard.removeObject(forKey: Constants.pushToken)
                    UserDefaults.standard.removeObject(forKey: Constants.deviceId)
                    completion?()
                }
            }
        } else {
            completion?()
        }
    }

    private func registerPushIfNeeded(with token: Data) {
        shouldRegisterToken(with: token) { shouldRegister in
            if shouldRegister {
                self.client.registerVoipToken(token, isSandbox: true) { error, deviceId in
                    if error == nil {
                        print("VPush: push token registered")
                        UserDefaults.standard.setValue(token, forKey: Constants.pushToken)
                        UserDefaults.standard.setValue(deviceId, forKey: Constants.deviceId)
                    } else {
                        print("VPush: registration error: \(String(describing: error))")
                        return
                    }
                }
            }
        }
    }

    private func shouldRegisterToken(with token: Data, completion: @escaping (Bool) -> Void) {
        let storedToken = UserDefaults.standard.object(forKey: Constants.pushToken) as? Data
        
        if let storedToken = storedToken, storedToken == token {
            completion(false)
            return
        }
        
        invalidatePushToken {
            completion(true)
        }
    }

La función registerPushIfNeeded toma un token y utiliza la función shouldRegisterToken para comprobar si el token ya ha sido registrado. Si no lo ha sido, registerVoipToken el cliente registrará la notificación push con Vonage. En la clase AppDelegate ya puedes registrarte para recibir notificaciones push de VoIP. Importa PushKit en la parte superior del archivo:

import PushKit

Añadir una instancia local de la ClientManager clase:

class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    private let clientManager = ClientManager.shared
    ...
}

Crea una nueva extensión al final del archivo que contenga una función para registrar el dispositivo para las notificaciones push:

extension AppDelegate: PKPushRegistryDelegate {
    func registerForVoIPPushes() {
        let voipRegistry = PKPushRegistry(queue: nil)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [PKPushType.voIP]
    }
}

Actualizar la función didFinishLaunchingWithOptions para llamar a la función registerForVoIPPushes e inicie sesión en el Client SDK:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    AVAudioSession.sharedInstance().requestRecordPermission { (granted:Bool) in
        print("Allow microphone use. Response: \(granted)")
    }
    registerForVoIPPushes()
    clientManager.login()
    return true
}

Añade las funciones PKPushRegistryDelegate funciones para gestionar el registro de notificaciones push a la extensión:

extension AppDelegate: PKPushRegistryDelegate {
    ...

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        clientManager.pushToken = pushCredentials.token
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        clientManager.invalidatePushToken(nil)
    }
}

El token push se almacena como una propiedad en la clase ClientManager ya que sólo deseas registrar el token con Vonage cuando el cliente inicia sesión, así que edita la función login en la clase ClientManager para manejar esto:

func login(isPushLogin: Bool = false) {
    print("VPush: Login - isPush:", isPushLogin)
    guard !isActiveCall else { return }
    
    ongoingPushLogin = isPushLogin
    
    getJWT { jwt in
        self.client.createSession(jwt) { error, sessionID in
            let statusText: String
            if error == nil {
                statusText = "Connected"
                
                if isPushLogin {
                    self.handlePushLogin()
                } else {
                    self.handleLogin()
                }
            } else {
                statusText = error!.localizedDescription
            }
            
            self.delegate?.clientStatusUpdated(self, status: statusText)
        }
    }
}

private func handlePushLogin() {
    ongoingPushLogin = false
    
    if let storedAction = storedAction {
        storedAction()
    }
}

private func handleLogin() {
    if let token = pushToken {
        registerPushIfNeeded(with: token)
    }
}

Gestión de las notificaciones push entrantes

Con el dispositivo registrado, ahora puede recibir notificaciones push de Vonage. El Client SDK tiene funciones para verificar si la carga útil de una notificación push es la esperada y para procesar la carga útil. Cuando se llama a processCallInvitePushData convierte la carga útil en una llamada que se recibe en la función didReceiveInviteForCall de la función VGVoiceClientDelegate.

Al igual que cuando se registra un token push, sólo se desea procesar un push entrante cuando se ha iniciado sesión en el Client SDK. Implemente las funciones en la clase ClientManager junto con una variable local para almacenar un push entrante:

final class ClientManager: NSObject {
    ...

    private var ongoingPushLogin = false
    private var ongoingPushKitCompletion: () -> Void = { }
    private var storedAction: (() -> Void)?

    ...

    func isVonagePush(with userInfo: [AnyHashable : Any]) -> Bool {
        VGVoiceClient.vonagePushType(userInfo) == .unknown ? false : true
    }

    func processPushPayload(with payload: [AnyHashable : Any], pushKitCompletion: @escaping () -> Void) -> String? {
        self.ongoingPushKitCompletion = pushKitCompletion
        return client.processCallInvitePushData(payload)
    }

    ...
}

La página PKPushRegistryDelegate tiene una función que se llama cuando hay un push entrante llamado didReceiveIncomingPushWith añadirlo a la extensión PKPushRegistryDelegate en el AppDelegate.swift archivo:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    if clientManager.isVonagePush(with: payload.dictionaryPayload) {
        clientManager.login(isPushLogin: true)
        _ = clientManager.processPushPayload(with: payload.dictionaryPayload, pushKitCompletion: completion)
    }
}

Se recomienda que realice un inicio de sesión cuando tenga una notificación push de VoIP entrante, que es por lo que login se llama aquí. Esto utiliza la lógica en la clase ClientManager que almacena la información sobre un push que se utilizará después de que se haya completado el inicio de sesión. La lógica será implementada en una etapa posterior.

Cuando tu aplicación iOS tiene una notificación push VoIP entrante, debes manejarla usando la clase CXProvider del framework CallKit. Crea un nuevo archivo Swift (CMD + N) llamado ProviderDelegate:

import CallKit
import AVFoundation
import VonageClientSDKVoice

final class ProviderDelegate: NSObject {
    private let provider: CXProvider
    private let callController = CXCallController()
    private var activeCall: UUID? = nil
    
    override init() {
        provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    static var providerConfiguration: CXProviderConfiguration = {
        let providerConfiguration = CXProviderConfiguration()
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportedHandleTypes = [.generic, .phoneNumber]
        return providerConfiguration
    }()
}

La propiedad callController es un objeto CXCallController utilizada por la clase para gestionar las acciones del usuario en la interfaz de usuario de CallKit. A continuación, cree una extensión al final del archivo para implementar la propiedad CXProviderDelegate:

extension ProviderDelegate: CXProviderDelegate {
   func providerDidReset(_ provider: CXProvider) {
        activeCall = nil
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        ClientManager.shared.answer(activeCall!.uuidString.lowercased()) { error in
            if error == nil {
                action.fulfill()
            } else {
                action.fail()
            }
        }
    }
    
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        hangup(action: action)
    }
    
    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        VGVoiceClient.enableAudio(audioSession)
    }
    
    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        VGVoiceClient.disableAudio(audioSession)
    }
}

Cuando CallKit activa y desactiva la sesión de audio, las funciones delegadas activan y desactivan el audio del Client SDK utilizando la sesión de audio de CallKit.

Cuando la interfaz de usuario de CallKit responde a la llamada, llama a la función delegada CXAnswerCallAction función delegada. Esta llama a la función answer que implementará en el ClientManager en un paso futuro.

CXEndCallAction se llama cuando la llamada finaliza desde la interfaz de usuario de CallKit, que llama a la función hangup que implementarás a continuación. Implementa el resto de las funciones necesarias en la clase ProviderDelegate clase:

final class ProviderDelegate: NSObject {
    ...
    
    func reportCall(_ callID: String, caller: String, completion: @escaping () -> Void) {
        activeCall = UUID(uuidString: callID)
        let update = CXCallUpdate()
        update.localizedCallerName = caller
        
        provider.reportNewIncomingCall(with: activeCall!, update: update) { error in
            if error == nil {
                completion()
            }
        }
    }
    
    func didReceiveHangup(_ callID: String) {
        let uuid = UUID(uuidString: callID)!
        provider.reportCall(with: uuid, endedAt: Date.now, reason: .remoteEnded)
    }
    
    func reportFailedCall(_ callID: String) {
        let uuid = UUID(uuidString: callID)!
        provider.reportCall(with: uuid, endedAt: Date.now, reason: .failed)
    }
    
    private func hangup(action: CXEndCallAction) {
        if activeCall == nil {
            endCallTransaction(action: action)
        } else {
            ClientManager.shared.reject(activeCall!.uuidString.lowercased()) { error in
                if error == nil {
                    self.endCallTransaction(action: action)
                }
            }
        }
    }
    
    private func endCallTransaction(action: CXEndCallAction) {
        self.callController.request(CXTransaction(action: action)) { error in
            if error == nil {
                self.activeCall = nil
                action.fulfill()
            } else {
                action.fail()
            }
        }
    }
}

La función reportCall función llama reportNewIncomingCall que activa la UI del sistema CallKit, las otras funciones ayudan a actualizar o finalizar las llamadas. Ahora que la ProviderDelegate está completa, puedes actualizar la clase ClientManager para utilizarla. Añade la propiedad providerDelegate al gestor de clientes:

final class ClientManager: NSObject {
    ...

    private let providerDelegate = ProviderDelegate()

    ...
}

A continuación, aplique el método VGVoiceClientDelegate:

extension ClientManager: VGVoiceClientDelegate {
    func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {
        print("VPush: Received invite", callId)
        providerDelegate.reportCall(callId, caller: caller, completion: ongoingPushKitCompletion)
    }
    
    func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {
        print("VPush: Received hangup")
        isActiveCall = false
        providerDelegate.didReceiveHangup(callId)
    }
    
    func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: String, with reason: VGVoiceInviteCancelReason) {
        print("VPush: Received invite cancel")
        providerDelegate.reportFailedCall(callId)
    }
    
    func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {
        let reasonString: String!
        
        switch reason {
        case .tokenExpired:
            reasonString = "Expired Token"
        case .pingTimeout, .transportClosed:
            reasonString = "Network Error"
        default:
            reasonString = "Unknown"
        }
        
        delegate?.clientStatusUpdated(self, status: reasonString)
    }
}

Después de que el SDK haya procesado la llamada, obtendrá una invitación de llamada en la función didReceiveInviteForCall que, a su vez, informará de la llamada. didReceiveHangupForCall y didReceiveInviteCancelForCall también llamarán a sus respectivas funciones en el delegado del proveedor. Para completar la clase ClientManager añada las funciones answer y reject :

func answer(_ callID: String, completion: @escaping (Error?) -> Void) {
    let answerAction = {
        print("VPush: Answer", callID)
        self.isActiveCall = true
        self.client.answer(callID, callback: completion)
    }
    
    if ongoingPushLogin {
        print("VPush: Storing answer")
        storedAction = answerAction
    } else {
        answerAction()
    }
    
}

func reject(_ callID: String, completion: @escaping (Error?) -> Void) {
    let rejectAction = {
        print("VPush: Reject", callID)
        self.isActiveCall = false
        self.client.reject(callID, callback: completion)
    }
    
    if ongoingPushLogin {
        print("VPush: Storing Reject")
        storedAction = rejectAction
    } else {
        rejectAction()
    }
}

De nuevo, sólo se puede responder o rechazar una llamada después de haber iniciado sesión en Client SDK. Ambas funciones utilizan la función ongoingPushLogin para comprobar si el inicio de sesión se ha completado con éxito; si no es así, la acción se almacena utilizando storedAction. Si observa la función handlePushLogin puede ver que cuando se completa el inicio de sesión, se llama a una acción almacenada si existe.

Pruébalo

Construye y ejecuta (CMD + R) el proyecto en tu dispositivo iOS (o simulador en Xcode 14 y superior), acepta los permisos del micrófono y bloquea el dispositivo. Luego llama al número vinculado a tu aplicación de Vonage de antes. Verás la llamada entrante directamente en tu pantalla de bloqueo; luego, una vez que la atiendas, pasará a la conocida pantalla de llamada de iOS:

incoming call with locked screen

active call from locked screen

Si compruebas los registros de llamadas del dispositivo, también verás que la llamada aparece allí.

¿Y ahora qué?

Puede encontrar el proyecto completo en GitHub. Puedes hacer mucho más con el Client SDK y CallKit; puedes usar CallKit para llamadas salientes. Obtén más información sobre el Client SDK en la página Descripción general del Client SDK de Vonage y sobre CallKit en developer.apple.com.

Compartir:

https://a.storyblok.com/f/270183/400x400/19c02db2d3/abdul-ajetunmobi.png
Abdul AjetunmobiVonage Antiguo miembro del equipo

Abdul es desarrollador de Vonage. Ha trabajado en productos de consumo como ingeniero de iOS. En su tiempo libre, le gusta andar en bicicleta, escuchar música y asesorar a aquellos que están comenzando su viaje en la tecnología.