Notifications push - iOS
Vue d'ensemble
iOS impose aux applications VoIP d'utiliser PushKit pour les notifications d'appels entrants et CallKit pour afficher l'interface utilisateur de l'appel système. Ces deux frameworks sont étroitement liés : Apple impose que chaque notification VoIP donne lieu à un reportNewIncomingCall à CallKit. Tout manquement à cette obligation entraîne la révocation définitive par iOS de la diffusion des notifications push sur l'appareil.
Le Vonage Client SDK s'intègre aux deux frameworks. Votre application doit :
- Inscription aux notifications VoIP et transmission du jeton à Vonage
- Réception des données de notification et transmission de celles-ci au SDK pour traitement
- Affichage des appels entrants via CallKit
- Gestion du cycle de vie des sessions audio exclusivement via les callbacks de CallKit
- Maintenir une session SDK valide afin de pouvoir répondre aux appels
Responsabilités liées aux composants
| Composant | Responsabilité |
|---|---|
| PushKit | Réactive l'application lorsqu'elle est arrêtée ou en veille, et transmet la charge utile de la notification push |
| SDK Vonage | Décode la charge utile des requêtes « push », gère la session WebRTC et traite la signalisation des appels |
| CallKit | Fournit une interface utilisateur pour les appels système et gère l'activation des sessions audio |
| Votre application | Coordonne les éléments ci-dessus et gère le cycle de vie des JWT et des sessions |
Configuration du projet
1 - Fonctionnalités d'Xcode
Activez les options suivantes dans le fichier Signature et capacités tabulation :
Push Notifications → enabled
Background Modes → Voice over IP (checked)
→ Background processing (checked)
Votre dossier de droits doit contenir :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.pushkit.unrestricted-voip</key>
<true/>
</dict>
</plist>
Avertissement : Les aps-environment Cette valeur est définie automatiquement par Xcode en fonction du profil de provisionnement. Ne la modifiez pas manuellement.
Droit d'accès restreint à la VoIP
À partir d'iOS 16, les applications sans les com.apple.developer.pushkit.unrestricted-voip Le nombre de notifications PushKit autorisées est limité à un petit nombre par jour. Si le budget alloué par le système est dépassé, l'envoi des notifications est automatiquement limité et les appels entrants ne réveilleront pas l'application.
Ce droit doit être demandé à Apple via le formulaire de demande de prestations. Tant que cette autorisation n'aura pas été accordée, les versions de développement fonctionneront normalement (les versions de débogage disposent d'un budget plus élevé), mais les versions de production seront limitées.
Assurez-vous que ce droit figure bien dans votre .entitlements fichier et approuvé par Apple avant sa mise en ligne sur l'App Store.
2 - Certificats Push
Vous avez besoin d'un certificat APNs pour votre App ID, exporté sous la forme d'un .p12, et téléchargé dans l’application Vonage auprès de laquelle votre application s’authentifie. Un certificat SSL APNs moderne, limité au périmètre de l’application, est valable pour la .voip sujet, c'est pourquoi un « certificat de services VoIP » distinct et hérité est pas obligatoire.
Générer un CSR
En Accès aux trousseaux → Assistant de certificats → Demander un certificat auprès d'une autorité de certification, saisissez votre adresse e-mail et un « nom commun », ne remplissez pas le champ « E-mail de l'autorité de certification », puis sélectionnez Enregistré sur le disque, puis enregistrez le .certSigningRequest.
Créer le certificat
- Dans le cadre de la Portail des développeurs Apple → Certificats, identifiants et profils, assurez-vous que votre ID de l'application a le Notifications push fonctionnalité activée.
- Sous Certificats, ajoutez un Service Apple Push Notification SSL (Sandbox & Production) certificat, sélectionnez votre identifiant d'application, puis téléchargez la CSR.
- Télécharger le fichier obtenu
.cer.
Exporter le .p12
Double-cliquez sur le .cer pour l'importer dans votre trousseau de mots de passe. Dans Mes certificats, trouver Apple Push Services: <your-bundle-id> — la clé privée doit se trouver à un niveau inférieur — puis cliquez avec le bouton droit de la souris → Exportation → Échange d'informations personnelles (.p12). Si possible, ne renseignez pas le mot de passe d'exportation (voir la remarque concernant le téléchargement ci-dessous).
Téléversement via le tableau de bord Vonage
- Se connecter à l'application Tableau de bord Vonage.
- Aller à Applications → votre application (le
application_id(sur lequel le JWT de votre application s'authentifie). - Ouvrir le Activer les notifications push onglet et téléchargez le
.p12. - Le champ « mot de passe » est facultatif et porte la mention non recommandé (Vonage le stocke avec votre clé privée), c'est pourquoi un certificat sans mot de passe est préférable.
Voir le Documentation sur la configuration des notifications push Vonage pour iOS à titre de référence.
Les certificats expirent chaque année. Le mode de défaillance est le suivant : total et silencieux — aucune erreur n'apparaît nulle part dans le système. Surveillez la date d'expiration et renouvelez de manière proactive.
Avertissement : Sandbox vs Production
Les versions de débogage d’Xcode utilisent l’environnement APNs « Sandbox » ; les versions TestFlight et App Store utilisent l’environnement « Production ». Télécharger certificats distincts pour chaque environnement et déterminer systématiquement le isSandbox indicateur au moment de la compilation à l'aide de #if DEBUG — ne l'inscrivez jamais en dur.
3 - Cadres
Ajouter CallKit sous Cadres, bibliothèques et contenu intégré dans l'onglet « Général » de votre cible. Importez les éléments nécessaires :
import PushKit
import CallKit
import VonageClientSDKVoice
import AVFoundation
4 - Initialisation du SDK
Initialiser VGVoiceClient une seule fois, au début du cycle de vie de l'application (par exemple, dans votre AppDelegate ou dans un gestionnaire singleton). Définissez son délégué avant de vous inscrire aux notifications push ou de créer des sessions :
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true
#endif
let client = VGVoiceClient(config)
client.delegate = self
Demandez l'autorisation d'accéder au microphone dès le début du cycle de vie de l'application — de préférence dans application(_:didFinishLaunchingWithOptions:):
AVAudioSession.sharedInstance().requestRecordPermission { granted in
print("Microphone permission: \(granted)")
}
Si l'utilisateur refuse l'accès au microphone, les appels s'établiront mais son interlocuteur n'entendra rien.
Intégration de PushKit
S'inscrire aux notifications VoIP
Créer un PKPushRegistry et de définir les types de notifications push souhaités au démarrage de l'application, avant que la moindre notification ne puisse arriver.
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(pushToken, withDeviceToken: nil, isSandbox: isSandbox) { error, deviceId in
if let error = error {
print("Failed to register push token: \(error.localizedDescription)")
return
}
// Store deviceId for unregistration on logout
print("Push token registered, deviceId: \(deviceId ?? "")")
}
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
PKPushRegistryDelegate
Implémentez les trois méthodes de délégué. La troisième — didReceiveIncomingPushWith — est le plus crucial :
extension AppDelegate: PKPushRegistryDelegate {
// Called when a new VoIP token is issued or rotated
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
guard type == .voIP else { return }
CallManager.shared.voipTokenDidUpdate(pushCredentials.token)
}
// Called when the token is invalidated (e.g. app reinstall)
func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType) {
CallManager.shared.unregisterDeviceToken()
}
// Called when an inbound VoIP push is received
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else { completion(); return }
CallManager.shared.handleIncomingPush(payload: payload, completion: completion)
}
}
Enregistrement du jeton auprès de Vonage
Inscrivez-vous auprès de client.registerVoipToken(_:isSandbox:) uniquement lorsqu'une session Vonage est active. Enregistrez la valeur renvoyée deviceId — vous en avez besoin pour vous désinscrire lorsque vous vous déconnectez.
import PushKit
let pushRegistry = PKPushRegistry(queue: .main)
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
#if DEBUG
BOOL isSandbox = YES;
#else
BOOL isSandbox = NO;
#endif
[client registerVoipToken:pushToken withDeviceToken:nil isSandbox:isSandbox callback:^(NSError * _Nullable error, NSString * _Nullable deviceId) {
if (error != nil) {
NSLog(@"Failed to register push token: %@", error);
return;
}
// Store deviceId for unregistration on logout
NSLog(@"Push token registered, deviceId: %@", deviceId);
}];
Ne pas appeler registerVoipToken à chaque réinitialisation de session. Une fois enregistré, le jeton reste valide jusqu’à ce qu’il soit renouvelé ou que l’utilisateur se déconnecte. Réenregistrer à chaque createSession accumule les entrées relatives aux appareils et peut entraîner des envois en double.
Gestion des sessions
Une session Vonage (client.createSession(jwt)) est nécessaire pour que le SDK puisse traiter les invitations d’appel ou effectuer des actions liées aux appels. Lorsqu’une notification VoIP arrive et que l’application est fermée ou que la session a expiré, celle-ci doit être recréée. La rapidité avec laquelle cela se produit détermine directement si les utilisateurs peuvent répondre aux appels avec succès.
La défaillance de production la plus courante
À ne pas faire :
À la réception d'une notification push : appeler votre serveur d'authentification → récupérer un nouveau JWT → appeler createSession. Cela ajoute deux sauts de réseau série dans une fenêtre d'exécution en arrière-plan où le signal est faible. L'utilisateur voit l'écran CallKit, appuie sur « Répondre » avant que la session ne soit prête, et l'appel échoue sans message d'erreur.
À faire :
Lors de la connexion de l'utilisateur : enregistrer le JWT (et un jeton de rafraîchissement) localement. À la réception d'une notification push : appeler createSession directement avec le JWT stocké. Aucun appel au backend dans le parcours rapide.
JWT Strategy
Suivre une chaîne de priorité — utiliser le chemin le plus rapide disponible :
| Priorité | État | Action | Latence typique |
|---|---|---|---|
| 1er | Une session est en cours | À rendre immédiatement — aucune intervention requise | 0 ms |
| 2e | Le JWT enregistré est valide | client.createSession(storedJWT) |
100 à 400 ms |
| 3e | Le JWT a expiré, un jeton d'actualisation est disponible | Actualiser le point de terminaison -> nouveau JWT -> createSession |
300 à 1 000 ms |
| 4e | Aucun identifiant | Avertissement du journal : impossible de répondre à l'appel | 0 ms |
Création de la session
do {
let sessionId = try await client.createSession(jwt)
print("Session created: \(sessionId)")
} catch {
print("Failed to create session: \(error.localizedDescription)")
}
[client createSession:jwt callback:^(NSError * _Nullable error, NSString * _Nullable sessionId) {
if (error != nil || sessionId == nil) {
NSLog(@"Failed to create session: %@", error);
return;
}
NSLog(@"Session created: %@", sessionId);
}];
Restauration de la session à la réception d'une notification push
func restoreSessionIfNeeded() {
guard activeSessionId == nil else {
print("Session already active")
return
}
if let jwt = UserDefaults.standard.string(forKey: "vonage_jwt") {
// Fast path — use stored JWT directly
createSession(jwt: jwt)
} else if let refreshToken = storedRefreshToken {
// Slow path — exchange refresh token for a new JWT first
refreshJWT(refreshToken) { [weak self] newJWT in
self?.createSession(jwt: newJWT)
}
} else {
print("No credentials available for session restoration")
}
}
Traitement des charges utiles « push »
C'est l'étape de l'intégration la plus sensible au timing. Deux opérations doivent être effectuées à partir de handleIncomingPush, et la relation d'ordre entre eux n'est pas évidente.
Le modèle de concurrence (obligatoire)
N'attendez jamais que la session soit rétablie avant d'appeler processCallInvitePushData. Mise en attente du traitement par poussée createSession retards reportNewIncomingCall par un aller-retour complet sur le réseau. Apple considère cela comme un échec de la gestion de la notification push : PushKit cesse alors discrètement d'envoyer de nouvelles notifications push vers l'appareil.
Mise en œuvre
client.processCallInvitePushData(payload.dictionaryPayload)
[client processCallInvitePushData:payload.dictionaryPayload];
Le modèle d'implémentation complet :
private var pendingPushCompletion: (() -> Void)?
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
// 1. Validate: only handle Vonage incoming-call pushes
let pushType = VGVoiceClient.vonagePushType(payload.dictionaryPayload)
guard pushType == .incomingCall else {
// Must still report to CallKit — failing to do so breaks future push delivery.
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
// 2. Store completion — will be called after reportNewIncomingCall
pendingPushCompletion = completion
// 3a. Restore session concurrently — non-blocking
restoreSessionIfNeeded()
// 3b. Process the push immediately — triggers didReceiveInviteForCall
client.processCallInvitePushData(payload.dictionaryPayload)
}
Avertissement : Même si la publication est mal formée, périmée ou en double, vous doit toujours appeler reportNewIncomingCall et mettre fin immédiatement à l'appel. Si vous ignorez cette étape, la transmission en temps réel sera définitivement interrompue.
Intégration de CallKit
Configuration de CXProvider
Créer CXProvider et CXCallController une seule fois et conservez-les pendant toute la durée de vie de votre gestionnaire. Ne les recréez pas à chaque appel ou à chaque mise à jour.
class CallManager: NSObject {
static let shared = CallManager()
private let callProvider: CXProvider
private let callController = CXCallController()
private override init() {
let config = CXProviderConfiguration()
config.supportsVideo = false
config.maximumCallGroups = 1
config.maximumCallsPerCallGroup = 1
config.supportedHandleTypes = [.generic]
config.supportsDTMF = true
config.supportsHolding = true
callProvider = CXProvider(configuration: config)
super.init()
callProvider.setDelegate(self, queue: nil)
}
}
Signaler un appel entrant
Appelé depuis didReceiveInviteForCall (voir la section « SDK Delegate »). La méthode de finalisation PushKit doit être appelée à l'intérieur de la reportNewIncomingCall rappel — jamais avant cela.
let update = CXCallUpdate()
let handleType: CXHandle.HandleType = channelType == .phone ? .phoneNumber : .generic
update.remoteHandle = CXHandle(type: handleType, value: caller)
update.hasVideo = false
provider.reportNewIncomingCall(with: callId, update: update) { error in
if let error = error {
print("Failed to report incoming call: \(error.localizedDescription)")
}
// Always call the PushKit completion inside this callback
pushCompletion()
}
CXCallUpdate *update = [[CXCallUpdate alloc] init];
CXHandleType handleType = (channelType == VGVoiceChannelTypePhone) ? CXHandleTypePhoneNumber : CXHandleTypeGeneric;
update.remoteHandle = [[CXHandle alloc] initWithType:handleType value:caller];
update.hasVideo = NO;
[provider reportNewIncomingCallWithUUID:callId update:update completion:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Failed to report incoming call: %@", error);
}
// Always call the PushKit completion inside this callback
pushCompletion();
}];
Type CXHandle
Utilisation .phoneNumber pour le type CXHandle lorsque VGVoiceChannelType est .phone (Appelants du réseau PSTN disposant de numéros E.164). Utilisez .generic pour les appels entre applications. Cela a une incidence sur la manière dont iOS affiche l'appelant dans l'interface utilisateur native des appels et dans la liste des appels récents.
CXProviderDelegate - Actions de l'utilisateur
extension CallManager: CXProviderDelegate {
// Called when CallKit resets (e.g. during system errors or when the
// provider is invalidated). Clean up all active call state.
func providerDidReset(_ provider: CXProvider) {
activeCall = nil
}
// User tapped Answer
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.answer(callId) { error in
if error == nil {
action.fulfill()
} else {
action.fail()
}
}
}
// User tapped End (covers both Reject while ringing and Hang up during call)
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
guard let callId = activeCallId else { action.fail(); return }
let complete: (Error?) -> Void = { error in
error == nil ? action.fulfill() : action.fail()
}
if isCallRinging {
client.reject(callId, callback: complete)
} else {
client.hangup(callId, callback: complete)
}
}
// Mute toggle
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isMuted {
client.mute(callId) { _ in action.fulfill() }
} else {
client.unmute(callId) { _ in action.fulfill() }
}
}
// Hold toggle
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isOnHold {
client.enableEarmuff(callId) { _ in action.fulfill() }
} else {
client.disableEarmuff(callId) { _ in action.fulfill() }
}
}
// DTMF
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.sendDTMF(callId, withDigits: action.digits) { _ in action.fulfill() }
}
}
Transfert de session audio
// In CXProviderDelegate - provider(_:didActivate:)
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
// In CXProviderDelegate - provider(_:didDeactivate:)
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
// In CXProviderDelegate - provider:didActivateAudioSession:
[VGVoiceClient enableAudio:audioSession];
// In CXProviderDelegate - provider:didDeactivateAudioSession:
[VGVoiceClient disableAudio:audioSession];
Toujours activer et désactiver l'audio Vonage exclusivement à l'intérieur de didActivate / didDeactivate. L'activation de l'audio avant que CallKit n'active la session, ou à partir d'un gestionnaire de bouton de l'interface utilisateur, provoque des bugs de routage audio avec Bluetooth, les AirPods et CarPlay.
Délégué SDK
VGVoiceClientDelegate - Événements liés aux appels entrants
extension CallManager: VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteForCall callId: VGCallId,
from caller: String,
with type: VGVoiceChannelType) {
guard let callUUID = UUID(uuidString: callId) else { return }
activeCallId = callId
activeCallUUID = callUUID
isCallRinging = true
// Report to CallKit, then call the PushKit completion in the callback
reportIncomingCall(callUUID: callUUID, caller: caller, type: type,
pushCompletion: pendingPushCompletion ?? {})
pendingPushCompletion = nil
}
// Remote party hung up or the call timed out
func voiceClient(_ client: VGVoiceClient,
didReceiveHangupForCall callId: VGCallId,
withQuality callQuality: VGRTCQuality,
reason: VGHangupReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .remoteReject: .declinedElsewhere
case .remoteHangup, .localHangup: .remoteEnded
case .remoteNoAnswerTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
// Caller cancelled the invite before it was answered
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteCancelForCall callId: VGCallId,
with reason: VGVoiceInviteCancelReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .answeredElsewhere: .answeredElsewhere
case .rejectedElsewhere: .declinedElsewhere
case .remoteCancel: .remoteEnded
case .remoteTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
private func cleanUpCallState() {
activeCallId = nil
activeCallUUID = nil
isCallRinging = false
}
}
Appelez toujours callProvider.reportCall(with:endedAt:reason:) pour chaque Chemin de terminaison — raccrochage à distance, annulation à distance, délai d’expiration de la connexion, rejet local. L’absence de l’un de ces éléments place CallKit dans un état incohérent et peut perturber le journal des appels ainsi que la génération future de rapports sur les appels.
Appels sortants
Pour les appels sortants, le processus est inverse : votre application lance l'appel via le SDK, puis utilise CXCallController pour informer CallKit.
// 1. Initiate the call via the SDK
func startCall(to callee: String) {
let callContext = ["callee": callee]
client.serverCall(callContext) { [weak self] error, callId in
guard let callId,
let callUUID = UUID(uuidString: callId) else { return }
self?.activeCallId = callId
self?.activeCallUUID = callUUID
// 2. Notify CallKit so the in-call UI appears
let handle = CXHandle(type: .generic, value: callee)
let action = CXStartCallAction(call: callUUID, handle: handle)
let transaction = CXTransaction(action: action)
self?.callController.request(transaction) { [weak self] error in
guard error == nil else { return }
self?.callProvider.reportOutgoingCall(with: callUUID, startedConnectingAt: .now)
}
}
}
// 3. CXStartCallAction — CallKit confirming the transaction
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
action.fulfill()
}
// 4. When the remote side answers
func voiceClient(_ client: VGVoiceClient,
didReceiveLegStatusUpdateForCall callId: VGCallId,
withLegId legId: String,
andStatus status: VGLegStatus) {
guard status == .answered, let uuid = activeCallUUID else { return }
callProvider.reportOutgoingCall(with: uuid, connectedAt: .now)
}
Gestion des erreurs
Le SDK se déclenche didReceiveSessionErrorWith(reason:) lorsque la connexion WebSocket sous-jacente est interrompue au cours d'une session active. Tentez toujours de rétablir automatiquement la session.
func client(_ client: VGBaseClient,
didReceiveSessionErrorWith reason: VGSessionErrorReason) {
print("Session error: \(reason)")
switch reason {
case .tokenExpired:
// The stored JWT is now invalid — skip it, use refresh token
refreshAndRestoreSession()
case .pingTimeout, .transportClosed:
// Transient network issue — try the stored JWT first
restoreSessionIfNeeded()
default:
restoreSessionIfNeeded()
}
}
private func refreshAndRestoreSession() {
guard let refreshToken = storedRefreshToken else {
clearSession()
return
}
refreshJWT(refreshToken) { [weak self] result in
switch result {
case .success(let newJWT):
self?.createSession(jwt: newJWT)
case .failure:
self?.clearSession()
}
}
}
Limites du simulateur
PushKit et CallKit ne sont pas disponibles sur le simulateur iOS. Le SDK propose un mode d'invitation via WebSocket pour le développement :
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true // receive invites over WebSocket — no push needed
#endif
let client = VGVoiceClient(config)
func answerCall(callId: String) {
#if targetEnvironment(simulator)
client.answer(callId) { error in
print("Answer: \(error?.localizedDescription ?? "OK")")
}
#else
guard let uuid = activeCallUUID else { return }
let action = CXAnswerCallAction(call: uuid)
callController.request(CXTransaction(action: action)) { _ in }
#endif
}
Avertissement : Testez toujours l'intégralité du parcours de push sur un appareil physique avant la mise en production. Le simulateur ne permet pas de tester le réveil après la fermeture de l'application, la restauration de la session dans le cadre d'un budget d'arrière-plan réel, ni l'interface utilisateur CallKit sur l'écran de verrouillage.
Pièges et anti-modèles
1 - Appel de la fonction « PushKit Completion » avant reportNewIncomingCall
À ne pas faire :
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
client.processCallInvitePushData(payload.dictionaryPayload)
completion() // WRONG — called before reportNewIncomingCall
}
Enregistrez la fonction de complétion et appelez-la exclusivement à l'intérieur de reportNewIncomingCallle callback asynchrone de .
2 - Saut à la corde reportNewIncomingCall pour les « push » non valides
À ne pas faire :
guard pushType == .incomingCall else {
completion() // WRONG — no call reported to CallKit
return
}
À faire : signaler un appel factice et y mettre fin immédiatement, puis appeler le numéro de confirmation :
guard pushType == .incomingCall else {
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
3 - Attente de la restauration de la session avant le traitement de la notification push
À ne pas faire :
restoreSessionIfNeeded { // Waits for network...
self.client.processCallInvitePushData(payload) // Too late
}
À faire : Tirez simultanément :
restoreSessionIfNeeded() // Non-blocking
client.processCallInvitePushData(payload.dictionaryPayload) // Immediate
4 - Récupération d'un JWT depuis votre backend à la réception d'une notification push
À ne pas faire :
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
fetchJWTFromServer { jwt in // 500ms - 3s network call
self.client.createSession(jwt) // Another network call
self.client.processCallInvitePushData(payload.dictionaryPayload)
}
}
À faire : enregistrer le JWT au moment de la connexion. L'utiliser directement lors de la réception d'une notification push, sans aucun appel au serveur.
5 - Codage en dur isSandbox: true
À ne pas faire :
client.registerVoipToken(token, isSandbox: true) // Breaks TestFlight
À faire :
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(token, isSandbox: isSandbox)
6 - Réenregistrement du jeton Push à chaque restauration de session
À ne pas faire :
func createSession(jwt: String) {
client.createSession(jwt) { _, _ in
self.registerVoipToken(self.storedToken) // Accumulates device entries
}
}
À faire : n'enregistrer que lors de la connexion de l'utilisateur ou lorsque didUpdate pushCredentials tirs (rotation des jetons).
7 - Oublier de se désinscrire lors de la déconnexion
Si vous passez cette étape unregisterDeviceTokens(byDeviceId:) Lorsqu'un utilisateur se déconnecte, l'appareil continue de recevoir indéfiniment les notifications VoIP destinées à cet utilisateur, même après la connexion d'un autre utilisateur.
8 - Non-mappage de tous les motifs de raccrochage vers CallKit
Chaque chemin de terminaison d'appel doit passer par callProvider.reportCall(with:endedAt:reason:) avec le bon CXCallEndedReason. L'absence d'un chemin d'accès corrompt le journal des appels système.
9 - Activation de l'audio en dehors des rappels CallKit
À ne pas faire :
func answerButtonTapped() {
VGVoiceClient.enableAudio(AVAudioSession.sharedInstance()) // WRONG
client.answer(callId) { _ in }
}
À faire : gérer l'audio exclusivement dans le délégué CallKit :
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
10 - Certificats Push périmés
Les certificats VoIP expirent au bout d'un an. En cas de défaillance, le système cesse complètement de fonctionner, sans aucun message d'alerte. Veillez à les renouveler de manière proactive et vérifiez la bonne réception de la notification push immédiatement après avoir téléchargé un nouveau certificat.
Liste de contrôle de production
Utilisez cette liste de contrôle avant l'expédition :
- [ ] Fonctionnalités Xcode configurées — Notifications push activées ; Modes d'arrière-plan : VoIP + Traitement en arrière-plan cochés
- [ ] Certificat VoIP « push » téléchargé pour les deux environnements — Environnement de test pour le débogage, environnement de production pour TestFlight/App Store
- [ ]
isSandboxutilisations du drapeau#if DEBUG— jamais codé en dur - [ ] Le JWT et le jeton de rafraîchissement sont conservés localement lors de la connexion — la restauration de session ne doit pas nécessiter d'appel vers le backend
- [ ]
reportNewIncomingCalla exigé que chaque notification VoIP soit transmise sans exception — y compris les notifications invalides, en double ou ne provenant pas de Vonage - [ ] La fonction de complétion PushKit n'est appelée qu'à l'intérieur de
reportNewIncomingCallla fonction de rappel de — jamais auparavant, jamais manqué - [ ] La restauration de session et le traitement en mode « push » s'exécutent simultanément -
restoreSessionIfNeeded()etprocessCallInvitePushData()tir de manière autonome - [ ] Enregistrement du jeton uniquement lors de la connexion de l'utilisateur ou lors de la rotation du jeton — pas à chaque fois
createSession - [ ]
unregisterDeviceTokensappelé lors d'une déconnexion explicite - [ ] Tous les chemins de terminaison d'appel sont signalés à CallKit — raccrochage à distance, annulation, délai d'attente, rejet local, défaillance du support
- [ ] Activation/désactivation du son uniquement dans
didActivate/didDeactivate - [ ] Code CallKit protégé par
#if !targetEnvironment(simulator)-enableWebsocketInvites = truepour simulateur - [ ] Testé sur un appareil physique après avoir complètement fermé l'application — Exercices : course à la sécurité PushKit (réveil et restauration de session)
- [ ] Testé sur un appareil physique avec l'application en arrière-plan (suspendue)
- [ ] Suivi de la date de renouvellement du certificat Push — les certificats expirent au bout d'un an