https://a.storyblok.com/f/270183/50478/20ae84179e/callkit-push-notifications.png

Como lidar com notificações push de VoIP usando o CallKit do iOS

Publicado em May 25, 2023

Tempo de leitura: 8 minutos

Neste tutorial, você utilizará CallKit para lidar com as notificações push de VoIP enviadas a um dispositivo iOS ao usar o Vonage Client SDK para iOS. As notificações push de VoIP são o principal método para receber chamadas recebidas com o Client SDK, já que elas chegarão ao dispositivo independentemente de seu aplicativo estar em primeiro plano ou não. O CallKit permite que você integre seu aplicativo iOS ao sistema, de modo que ele pareça uma chamada telefônica nativa do iOS. Usar o Vonage Client SDK com o CallKit permitirá que você incorpore funcionalidades de chamada ao seu aplicativo, mantendo uma interface consistente e familiar para chamadas recebidas.

Pré-requisitos

  • Um account da API da Vonage. Se você ainda não tiver um, pode se cadastrar hoje mesmo.

  • Um account de desenvolvedor da Apple e um dispositivo iOS (ou simulador no Xcode 14 e versões posteriores).

  • Um account no GitHub.

  • Xcode 12 e Swift 5 ou versão posterior.

  • Cocoapods para instalar o Vonage Client SDK para iOS.

  • Nossa interface de linha de comando. Você pode instalá-la com npm install -g @vonage/cli.

O Projeto Inicial

Este blog se baseará no tutorial “Recebendo uma chamada telefônica no aplicativo” do portal de desenvolvedores da Vonage. Este tutorial começará a partir do estado final do projeto do tutorial. Você pode acompanhar passo a passo ou, se já estiver familiarizado com a criação de um aplicativo de voz usando o Vonage Client SDK, pode clonar o no GitHub.

Configurar certificados de notificação push

Existem dois tipos de notificações push que você pode usar em um aplicativo iOS: notificações push de VoIP com o PushKit ou Notificações do Usuário. Este tutorial se concentrará nas notificações push de VoIP. O serviço de Notificações Push da Apple (APNs) usa autenticação baseada em certificado para proteger as conexões entre o APNs e os servidores da Vonage. Portanto, você precisará criar um certificado e enviá-lo aos servidores da Vonage para que a empresa possa enviar uma notificação push ao dispositivo quando houver uma chamada recebida.

Incorporando a funcionalidade de notificações push

Para usar notificações push, é necessário adicionar o recurso de notificações push ao seu projeto do Xcode. Certifique-se de estar conectado à sua conta de desenvolvedor da Apple no Xcode por meio das preferências. Se estiver, selecione seu destino e, em seguida, escolha Assinatura e Recursos:

signing and capabilities tag

Em seguida, selecione “Adicionar recurso” e adicione as Notificações push e Modos em segundo plano :

add capability button

Na funcionalidade “Modos de fundo”, selecione Voz sobre IP e Processamento em segundo plano. Se o Xcode estiver gerenciando automaticamente a assinatura do seu aplicativo, ele atualizará o perfil de provisionamento vinculado ao seu Identificador de Pacote para incluir esses recursos.

Ao utilizar notificações push via VoIP, é necessário usar o framework CallKit. Vincule-o ao seu projeto adicionando-o em Frameworks, Bibliotecas e Conteúdo Incorporado em Geral:

add callkit framework

Geração de um certificado de push

Para gerar um certificado push, você precisará fazer login na sua conta de desenvolvedor da Apple e acessar a seção página “Certificados, Identificadores e Perfis” e adicionar um novo certificado:

add certificate button

Escolha Serviço de Notificação Push da Apple SSL (Sandbox e Produção) e continue.

Certificate wizard checkbox

Agora você precisará escolher o App ID do aplicativo ao qual deseja adicionar notificações push de VoIP e continuar. Se o seu aplicativo não estiver na lista, você precisará criar um App ID. O Xcode pode fazer isso por você, caso gerencie automaticamente sua assinatura. Caso contrário, você pode criar um novo App ID na seção página “Certificados, Identificadores e Perfis” , na seção Identificadores. Certifique-se de selecionar o recurso de notificações push ao fazer isso.

Será solicitado que você envie uma Solicitação de Assinatura de Certificado (CSR). Você pode seguir as instruções no site de ajuda da Apple para criar um CSR no seu Mac. Assim que o CSR for enviado, você poderá baixar o certificado. Clique duas vezes no .cer arquivo para instalá-lo no Acesso ao Chaveiro.

Para obter o certificado de push no formato exigido pelos servidores da Vonage, você precisará exportá-lo. Localize o certificado dos Serviços de VoIP no Acesso ao Chaveiro e clique com o botão direito do mouse para exportá-lo. Nomeie a exportação applecert e selecione .p12 como formato:

keychain access export

Carregue seu certificado de envio

Você fazer o upload seu certificado no Vonage usando o Painel da API. Abra seu aplicativo no painele, em seguida, abra a opção “Ativar notificações notificações" :

Push Upload on the dashboardPush Upload on the dashboard

Você pode enviar seu certificado .p12 da etapa anterior e adicionar uma senha, se necessário.

A classe ClientManager

Crie um novo arquivo Swift (CMD + N) e nomeie-o como ClientManager. Essa classe irá encapsular o código necessário para fazer a interface com o Client SDK, já que você precisará obter informações do Client SDK em vários pontos nas etapas seguintes:

Substitua ALICE_JWT pelo JWT que você gerou anteriormente; em um ambiente de produção, é aqui que você obteria um JWT do seu servidor de autenticação ou endpoint.

Com essa nova classe, você precisará mover o código de chamada do Client SDK da ViewController classe para a ClientManager classe. As duas classes se comunicarão com os ClientManagerDelegate observadores. Faça as seguintes alterações na sua ViewController classe:

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
        }
    }
}

Inscreva-se para receber notificações push

O próximo passo é registrar um dispositivo para notificações push, a fim de informar à Vonage para qual dispositivo enviar a notificação push para cada usuário. Na ClientManager classe, adicione a pushToken propriedade e as seguintes funções para lidar com o token push do 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)
        }
    }

A registerPushIfNeeded função recebe um token e, em seguida, usa a shouldRegisterToken função para verificar se o token já foi registrado. Caso contrário, registerVoipToken no cliente, a notificação push será registrada na Vonage. Na AppDelegate classe, agora você pode se cadastrar para receber notificações push de VoIP. Importe PushKit no início do arquivo:

import PushKit

Adicione uma instância local da ClientManager classe:

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

Crie uma nova extensão no final do arquivo que contenha uma função para registrar o dispositivo para notificações push:

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

Atualize a didFinishLaunchingWithOptions função para chamar a registerForVoIPPushes função e faça login no 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
}

Adicione as PKPushRegistryDelegate funções para lidar com o registro de notificações push à extensão:

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)
    }
}

O token de push é armazenado como uma propriedade na ClientManager classe, já que você deseja registrar o token na Vonage apenas quando o cliente estiver conectado; portanto, edite a login função na ClientManager classe para lidar com isso:

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)
    }
}

Tratar notificações push recebidas

Com o dispositivo registrado, ele agora pode receber notificações push da Vonage. O Client SDK possui funções para verificar se a carga útil de uma notificação push é a esperada e para processá-la. Quando processCallInvitePushData é chamado, ele converte a carga útil em uma chamada que é recebida na didReceiveInviteForCall função do VGVoiceClientDelegate.

Assim como no registro de um token de push, você só deve processar um push recebido quando o Client SDK estiver conectado. Implemente as funções na ClientManager classe, juntamente com uma variável local para armazenar uma notificação push recebida:

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)
    }

    ...
}

O PKPushRegistryDelegate possui uma função que é chamada quando há uma notificação recebida, chamada didReceiveIncomingPushWith adicione isso à extensão PKPushRegistryDelegate no AppDelegate.swift arquivo:

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)
    }
}

Recomenda-se que você faça o login ao receber uma notificação push de VoIP; é por isso que login é chamada aqui. Isso utiliza a lógica da ClientManager classe que armazena informações sobre um push a ser utilizadas após a conclusão do login. A lógica será implementada posteriormente.

Quando seu aplicativo iOS receber uma notificação push de VoIP, você deverá tratá-la usando a CXProvider classe do framework CallKit. Crie um novo arquivo Swift (CMD + N) chamado 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
    }()
}

A callController propriedade é um CXCallController objeto usado pela classe para lidar com as ações do usuário na interface do CallKit. Em seguida, crie uma extensão no final do arquivo para implementar o 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)
    }
}

Quando o CallKit ativa e desativa a sessão de áudio, as funções do delegado ativam e desativam o áudio do Client SDK por meio da sessão de áudio do CallKit.

Quando a interface do CallKit atende a chamada, ela chama a CXAnswerCallAction função delegada. Isso chama a answer função que você implementará em ClientManager em uma etapa futura.

CXEndCallAction é chamada quando a chamada é encerrada a partir da interface do CallKit, que chama a hangup função que você implementará a seguir. Implemente o restante das funções necessárias na ProviderDelegate classe:

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()
            }
        }
    }
}

As reportCall chamadas de função reportNewIncomingCall que aciona a interface do usuário do sistema CallKit; as outras funções servem para atualizar ou encerrar chamadas. Agora que a ProviderDelegate classe está pronta, você pode atualizar a ClientManager classe para utilizá-la. Adicione a providerDelegate propriedade ao gerenciador de clientes:

final class ClientManager: NSObject {
    ...

    private let providerDelegate = ProviderDelegate()

    ...
}

Em seguida, implemente o 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)
    }
}

Depois que o SDK processar a chamada, você receberá um convite para a chamada na didReceiveInviteForCall função delegada, que, por sua vez, informará sobre a chamada. didReceiveHangupForCall e didReceiveInviteCancelForCall também chamarão suas respectivas funções no delegado do provedor. Para completar a ClientManager classe, adicione o answer e reject funções:

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()
    }
}

Mais uma vez, só é possível atender ou rejeitar uma chamada após o Client SDK ter feito o login. Ambas as funções utilizam o ongoingPushLogin para verificar se o login foi concluído com sucesso; caso contrário, a ação é armazenada por meio de storedAction. Se você observar a handlePushLogin função, você verá que, quando o login é concluído, ela chama uma ação armazenada, caso exista uma.

Experimente

Compile e execute (CMD + R) o projeto no seu dispositivo iOS (ou no simulador no Xcode 14 e versões posteriores), aceite as permissões de microfone e bloqueie o dispositivo. Em seguida, ligue para o número vinculado ao seu aplicativo Vonage, conforme feito anteriormente. Você verá a chamada recebida diretamente na tela de bloqueio; depois, ao atendê-la, será direcionado para a conhecida tela de chamada do iOS:

incoming call with locked screen

active call from locked screen

Se você verificar o histórico de chamadas no aparelho, também verá essa chamada listada lá.

E agora?

Você pode encontrar o projeto concluído no GitHub. É possível fazer muito mais com o Client SDK e o CallKit; você pode usar o CallKit para chamadas de saída. Saiba mais sobre o Client SDK na Visão geral do Vonage Client SDK e sobre o CallKit em developer.apple.com.

Compartilhar:

https://a.storyblok.com/f/270183/400x400/19c02db2d3/abdul-ajetunmobi.png
Abdul AjetunmobiEx-funcionário da Vonage

Abdul é um Developer Advocate da Vonage. Ele tem experiência profissional na área de produtos de consumo como engenheiro de iOS. Em seu tempo livre, ele gosta de andar de bicicleta, ouvir música e orientar quem está dando os primeiros passos na área de tecnologia