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

Comment gérer les notifications push de VoIP avec iOS Callkit

Publié le May 25, 2023

Temps de lecture : 8 minutes

Dans ce tutoriel, vous utiliserez CallKit pour gérer les notifications push VoIP envoyées à un appareil iOS lors de l'utilisation du Client Vonage Client SDK pour iOS. Les notifications push VoIP constituent la principale méthode de réception des appels entrants avec le Client SDK puisqu'elles atteindront l'appareil, que votre application soit au premier plan ou non. CallKit vous permet d'intégrer votre application iOS au système afin que votre application puisse ressembler à un appel téléphonique iOS natif. L'utilisation du Client SDK de Vonage avec Callkit vous permettra d'intégrer les appels dans votre application tout en ayant une interface cohérente et familière pour les appels entrants.

Conditions préalables

  • Un Account API Vonage. Si vous n'en avez pas encore, vous pouvez vous inscrire aujourd'hui.

  • Un compte Apple Developer et un appareil iOS (ou un simulateur dans Xcode 14 et plus).

  • Un Account GitHub.

  • Xcode 12 et Swift 5 ou supérieur.

  • Cocoapods pour installer le Vonage Client SDK pour iOS.

  • Notre interface de ligne de commande. Vous pouvez l'installer avec npm install -g @vonage/cli.

Le projet Starter

Ce blog s'appuiera sur l'article "Recevoir un appel téléphonique dans l'application". "Recevoir un appel téléphonique in-app" du portail des développeurs de Vonage. Ce tutoriel commencera à partir de l'état fini du projet du tutoriel. Soit vous suivez, soit si vous êtes déjà familier avec la construction d'une application vocale Vonage Client SDK, vous pouvez cloner le projet de démarrage depuis GitHub.

Mise en place de certificats de poussée

Il existe deux types de notifications push que vous pouvez utiliser dans une application iOS, les push VoIP avec PushKit ou les notifications utilisateur. Ce tutoriel se concentrera sur les notifications VoIP. Le service Apple Push Notifications (APNs) utilise l'authentification par certificat pour sécuriser les connexions entre les APNs et les serveurs Vonage. Vous devrez donc créer un certificat et le télécharger vers les serveurs Vonage afin que Vonage puisse envoyer un push à l'appareil en cas d'appel entrant.

Ajout d'une capacité de notification push

Pour utiliser les notifications push, vous devez ajouter la capacité de notification push à votre projet Xcode. Assurez-vous d'être connecté à votre Account de développeur Apple dans Xcode via les préférences. Si c'est le cas, sélectionnez votre cible, puis choisissez Signing & Capabilities:

signing and capabilities tag

Sélectionnez ensuite "Ajouter une capacité" et ajoutez le champ "Notifications push". Notifications push et Modes d'arrière-plan :

add capability button

Sous la rubrique Modes d'arrière-plan, sélectionnez Voice over IP et Traitement en arrière-plan. Si Xcode gère automatiquement la signature de votre application, il mettra à jour le profil de provisionnement lié à votre identifiant d'ensemble pour inclure les capacités.

Lorsque vous utilisez des notifications push VoIP, vous devez utiliser le framework CallKit. Liez-le à votre projet en l'ajoutant sous Frameworks, Libraries, and Embedded Content (Cadres, bibliothèques et contenu intégré) sous Général :

add callkit framework

Générer un certificat Push

Pour générer un certificat push, vous devez vous connecter à votre Account de développeur Apple et vous rendre dans la section Certificats, identifiants et profils et ajouter un nouveau certificat :

add certificate button

Choisir Apple Push Notification service SSL (Sandbox & Production) et continuez.

Certificate wizard checkbox

Vous devez maintenant choisir l'App ID de l'application à laquelle vous souhaitez ajouter des notifications VoIP push et continuer. Si votre application n'est pas listée, vous devrez créer un App ID. Xcode peut le faire pour vous s'il gère automatiquement votre signature. Dans le cas contraire, vous pouvez créer un nouvel identifiant d'application dans la section Certificats, identifiants et profils sous Identifiants. Veillez à sélectionner la capacité de notifications push lors de cette opération.

Vous serez invité à télécharger une demande de signature de certificat (CSR). Vous pouvez suivre les instructions sur site d'aide d'Apple pour créer une CSR sur votre Mac. Une fois la CSR téléchargée, vous pourrez télécharger le certificat. Double-cliquez sur le fichier .cer pour l'installer dans le trousseau d'accès.

Pour obtenir le certificat push dans le format requis par les serveurs Vonage, vous devez l'exporter. Localisez votre certificat VoIP Services dans Keychain Access et cliquez avec le bouton droit de la souris pour l'exporter. Nommez l'exportation applecert et sélectionnez .p12 comme format :

keychain access export

Téléchargez votre certificat Push

Vous téléchargez votre certificat à Vonage à l'aide du le tableau de bord de l'API. Ouvrez votre application sur le tableau de bordpuis ouvrez la fenêtre "Enable notifications notifications" :

Push Upload on the dashboardPush Upload on the dashboard

Vous pouvez télécharger votre certificat .p12 de l'étape précédente et ajouter un mot de passe si nécessaire.

La classe ClientManager

Créez un nouveau fichier Swift (CMD + N) et l'appeler ClientManager. Cette classe encapsulera le code nécessaire à l'interface avec le Client SDK puisque vous aurez besoin d'obtenir des informations du Client SDK à plusieurs endroits dans les étapes suivantes :

Remplacez ALICE_JWT par le JWT que vous avez généré précédemment. Dans un environnement de production, c'est ici que vous récupérerez un JWT de votre serveur/terminal d'authentification.

Avec cette nouvelle classe, vous devrez déplacer le code du Client SDK de la classe ViewController vers la classe ClientManager dans la classe Les deux classes communiqueront avec les ClientManagerDelegate observateurs. Apportez les modifications suivantes à votre classe 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
        }
    }
}

S'inscrire aux notifications push

L'étape suivante consiste à enregistrer un appareil pour les notifications push afin de permettre à Vonage de savoir à quel appareil envoyer la notification push pour quel utilisateur. Dans la classe ClientManager ajoutez la propriété pushToken et les fonctions suivantes pour gérer le jeton push de l'appareil :

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 fonction registerPushIfNeeded prend un jeton et utilise ensuite la fonction shouldRegisterToken pour vérifier si le jeton a déjà été enregistré. Si ce n'est pas le cas, registerVoipToken sur le client enregistrera la notification push avec Vonage. Dans la classe AppDelegate vous pouvez maintenant vous enregistrer pour les notifications push VoIP. Importer PushKit en haut du fichier :

import PushKit

Ajouter une instance locale de la ClientManager classe :

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

Créez une nouvelle extension à la fin du fichier, qui contient une fonction permettant d'enregistrer l'appareil pour les notifications push :

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

Mettre à jour la fonction didFinishLaunchingWithOptions pour appeler la fonction registerForVoIPPushes et se connecter au 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
}

Ajouter les PKPushRegistryDelegate pour gérer l'enregistrement des notifications push dans l'extension :

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

Le jeton de poussée est stocké en tant que propriété de la classe ClientManager car vous ne voulez enregistrer le jeton avec Vonage que lorsque le client est connecté. login dans la classe ClientManager pour gérer cela :

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

Gérer les notifications push entrantes

Une fois l'appareil enregistré, il peut maintenant recevoir des notifications push de Vonage. Le Client SDK dispose de fonctions permettant de vérifier si la charge utile d'une notification push est celle attendue et de traiter la charge utile. Lorsque la fonction processCallInvitePushData est appelée, elle convertit la charge utile en un appel qui est reçu sur la fonction didReceiveInviteForCall de la fonction VGVoiceClientDelegate.

Comme pour l'enregistrement d'un jeton de poussée, vous ne voulez traiter une poussée entrante que lorsque le Client SDK a été connecté. Mettez en œuvre les fonctions de la classe ClientManager avec une variable locale pour stocker un push entrant :

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

    ...
}

Le PKPushRegistryDelegate possède une fonction qui est appelée lorsqu'il y a un "push" entrant appelé didReceiveIncomingPushWith l'ajouter à l'extension PKPushRegistryDelegate dans le AppDelegate.swift fichier :

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

Il est recommandé d'effectuer une connexion lorsque vous recevez une notification push VoIP entrante, c'est pourquoi login est appelé ici. Ceci utilise la logique de la classe ClientManager qui stocke les informations relatives à la notification push à utiliser une fois la connexion terminée. La logique sera mise en œuvre ultérieurement.

Lorsque votre application iOS reçoit une notification push VoIP entrante, vous devez la gérer à l'aide de la classe CXProvider du framework CallKit. Créez un nouveau fichier Swift (CMD + N) appelé 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
    }()
}

Le bien callController est un objet CXCallController utilisé par la classe pour gérer les actions de l'utilisateur sur l'interface CallKit. Ensuite, créez une extension à la fin du fichier pour implémenter la propriété 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)
    }
}

Lorsque CallKit active et désactive la session audio, les fonctions déléguées activent et désactivent l'audio du Client SDK à l'aide de la session audio CallKit.

Lorsque l'interface utilisateur CallKit répond à l'appel, elle appelle la fonction CXAnswerCallAction fonction déléguée. Celle-ci appelle la fonction answer que vous mettrez en œuvre sur la fonction ClientManager dans une étape ultérieure.

CXEndCallAction est appelée lorsque l'appel est terminé depuis l'interface utilisateur CallKit, qui appelle la fonction hangup que vous implémenterez ensuite. Implémentez le reste des fonctions nécessaires dans la 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()
            }
        }
    }
}

La fonction reportCall fonction calls reportNewIncomingCall qui déclenche l'interface utilisateur du système CallKit, les autres fonctions permettent de mettre à jour ou de terminer les appels. Maintenant que la ProviderDelegate est complète, vous pouvez mettre à jour la classe ClientManager pour l'utiliser. Ajoutez la propriété providerDelegate au gestionnaire de clients :

final class ClientManager: NSObject {
    ...

    private let providerDelegate = ProviderDelegate()

    ...
}

Ensuite, il faut mettre en œuvre l'option 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)
    }
}

Une fois que le SDK a traité l'appel, vous recevrez une invitation à l'appel sur la fonction déléguée didReceiveInviteForCall qui, à son tour, signalera l'appel. didReceiveHangupForCall et didReceiveInviteCancelForCall appellent également leurs fonctions respectives sur le délégué du fournisseur. Pour compléter la classe ClientManager ajoutez les éléments answer et reject pour compléter la classe :

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

Là encore, vous ne pouvez répondre à un appel ou le rejeter qu'après avoir ouvert une session dans le Client SDK. Les deux fonctions utilisent l'élément ongoingPushLogin pour vérifier si la connexion a été effectuée avec succès ; si ce n'est pas le cas, l'action est enregistrée à l'aide de la fonction storedAction. Si vous examinez la fonction handlePushLogin vous pouvez voir que lorsque la connexion est terminée, elle appelle une action stockée s'il y en a une.

Essayez-le

Construisez et exécutez (CMD + R) le projet sur votre appareil iOS (ou simulateur dans Xcode 14 et supérieur), acceptez les permissions du microphone et verrouillez l'appareil. Appelez ensuite le numéro lié à votre Applications Vonage de tout à l'heure. Vous verrez l'appel entrant directement sur votre écran de verrouillage ; puis, lorsque vous décrocherez, l'écran d'appel familier d'iOS s'affichera :

incoming call with locked screen

active call from locked screen

Si vous consultez le journal des appels sur l'appareil, vous verrez également l'appel répertorié.

Quelle est la prochaine étape ?

Vous pouvez trouver le projet terminé sur GitHub. Vous pouvez faire beaucoup plus avec le Client SDK et CallKit ; vous pouvez utiliser CallKit pour les appels sortants. Pour en savoir plus sur le Client SDK, consultez la page Aperçu du Client SDK de Vonage et CallKit sur developer.apple.com.

Partager:

https://a.storyblok.com/f/270183/400x400/19c02db2d3/abdul-ajetunmobi.png
Abdul AjetunmobiVonage Ancien membre de l'équipe

Abdul est défenseur des développeurs chez Vonage. Il a travaillé dans le domaine des produits de consommation en tant qu'ingénieur iOS. Pendant son temps libre, il aime faire du vélo, écouter de la musique et conseiller ceux qui commencent leur parcours dans la technologie.