SDK mobile headless per iOS: inizia

Questo documento spiega come iniziare a utilizzare l'SDK mobile headless per iOS.

L'SDK Headless Mobile per iOS ti consente di aggiungere l'assistenza basata sulla piattaforma CCAI direttamente nella tua esperienza iOS integrata senza utilizzare un'interfaccia utente della piattaforma CCAI predefinita.

Invece di presentare un widget con il brand CCAI Platform, devi:

  • Integra l'SDK CCAI per iOS nella tua app.

  • Crea le tue schermate, la navigazione e il design visivo.

  • Utilizza l'SDK per:

    • Avviare e gestire sessioni di chat e vocali.

    • Offri email, chiamate programmate e opzioni di deviazione.

    • Supporta le azioni intelligenti, gli allegati e i flussi post-sessione (CSAT, sondaggi, agente virtuale).

CCAI Platform gestisce la logica del contact center (routing, code, canali, configurazione, report). Sei il proprietario dell'intera esperienza dell'app (UI, flussi e branding).

Se preferisci un'interfaccia utente configurabile predefinita anziché crearne una personalizzata, utilizza l'SDK mobile standard.

Requisiti

Questi sono i requisiti per integrare l'SDK Headless Mobile per iOS.

Requisiti della piattaforma iOS

  • Versione minima di iOS: iOS 16.6 o versioni successive

  • Linguaggio: Swift

  • IDE: l'ultima versione stabile di Xcode supportata dalla tua app

  • Architetture: architetture iOS standard supportate dalla toolchain Xcode corrente.

Autorizzazioni app

Poiché l'app integrata gestisce la UI per le azioni intelligenti (acquisizione di foto, registrazione di video o verifica dell'identità), devi dichiarare le autorizzazioni per la privacy obbligatorie di Apple.

Aggiungi le seguenti chiavi al tuo Info.plist con una descrizione di come la tua app utilizza i dati:

  • NSCameraUsageDescription: necessario per consentire agli utenti di scattare e inviare foto/video o per le azioni intelligenti richieste dall'agente.

  • NSMicrophoneUsageDescription: necessario per le chiamate vocali istantanee e per registrare video con audio.

  • NSPhotoLibraryUsageDescription: obbligatorio per consentire agli utenti di caricare foto o video esistenti dal rullino.

  • NSFaceIDUsageDescription: obbligatorio se supporti le azioni intelligenti di verifica biometrica.

Requisiti per l'istanza della piattaforma CCAI

Per ottenere i seguenti dettagli, devi accedere alle impostazioni sviluppatore della tua istanza della piattaforma CCAI:

  • Chiave dell'azienda

  • Codice segreto dell'azienda

  • URL host: il nome host per la piattaforma CCAI (ad esempio, your_subdomain.ccaiplatform.com)

Questi valori vengono utilizzati per:

  • Chiave dell'azienda + URL host: nella tua app per iOS per inizializzare l'SDK.

  • Codice segreto dell'azienda: nel backend per firmare i token JWT (JSON Web Token) utilizzati per autenticare gli utenti.

Notifiche push

Per le chiamate e alcune azioni intelligenti, devi:

  • Configurazione del servizio di notifiche push di Apple (APNs) per le notifiche push standard.

  • Un certificato di servizi VoIP per supportare le chiamate in arrivo e notifiche specifiche relative alle chiamate.

Imparerai a:

  • Configura le credenziali APN e VoIP nel portale per sviluppatori Apple.

  • Carica i certificati convertiti nel portale amministratore della piattaforma CCAI in Impostazioni > Impostazioni sviluppatore > App mobile.

  • Registrati per le notifiche remote e i push VoIP nella tua app.

  • Inoltra i token dispositivo appropriati all'SDK.

  • In Target > Signing & capabilities, attiva le seguenti opzioni:

    • Funzionalità Notifiche push.

    • Funzionalità Modalità in background con questi elementi selezionati:

      • Voce over IP: richiesto per le notifiche push VoIP (chiamate in arrivo, azioni intelligenti che utilizzano la rete PSTN).

      • Audio, AirPlay e Picture in Picture: necessari per continuare l'audio della chiamata vocale quando l'app viene eseguita in background.

      • Notifiche remote: necessarie per la pubblicazione push APNs standard.

Come si integra l'SDK iOS headless nella tua app

L'SDK mobile headless per iOS può essere visualizzato come tre livelli:

Piattaforma e SDK CCAI Platform

L'SDK interagisce in modo sicuro con la piattaforma CCAI Platform e fornisce:

  • Autenticazione tramite token web JSON (JWT).

  • Metadati di routing di code e canali (chat, voce, chiamata pianificata, email, link di deviazione esterni).

  • Gestione dello stato della sessione in tempo reale.

  • La pipeline di trasporto sicura per azioni intelligenti, allegati e condivisione dello schermo.

  • Logica di deviazione e valutazione della gerarchia post-sessione.

La tua app per iOS (UI e logica gestite da te)

Controlli l'intera esperienza visiva e di navigazione:

  • Punti di accesso: ad esempio, una scheda Guida o un pulsante Contattaci.

  • Menu: come vengono visualizzate le code (ad esempio Fatturazione, Assistenza tecnica).

  • Interfaccia utente in sessione: le bolle di chat, le schermate di chiamata e i controlli personalizzati.

  • Cosa fa la tua app: chiama le API SDK per avviare e terminare le sessioni, ascolta gli eventi SDK e gestisce la navigazione del controller di visualizzazione.

Configurazione del portale amministratore di CCAI Platform

Gli amministratori di CCAI Platform configurano le regole di coinvolgimento (orari di apertura, canali disponibili, soglie di tempo di attesa e sondaggi). L'SDK legge questa configurazione e la espone alla tua app come dati ed eventi non elaborati. Decidi tu come disegnare la configurazione sullo schermo.

Recuperare le credenziali aziendali

Prima di integrare l'SDK, ottieni le credenziali dall'istanza della piattaforma CCAI:

  1. Accedi al portale amministratore della piattaforma CCAI con un account amministratore.

  2. Vai a Impostazioni > Impostazioni sviluppatore.

  3. Nella sezione Chiave e codice segreto dell'azienda, copia:

    • Chiave dell'azienda

    • Codice segreto dell'azienda

  4. Prendi nota dell'URL host:il nome host per la piattaforma CCAI (ad esempio, your_subdomain.ccaiplatform.com).

Utilizzerai:

  • Nella tua app per iOS: chiave aziendale + URL host.

  • Nel tuo server di backend: codice segreto dell'azienda per firmare i JWT per:

    • Autenticazione utente finale.

    • Dati o contesto personalizzati facoltativi utilizzati da CCAI Platform.

Aggiungere l'SDK alla tua app per iOS

L'SDK headless è creato utilizzando un'architettura modulare. Installa il modulo core CCAIKit insieme ai moduli delle funzionalità specifiche richieste dalla tua app (ad esempio, CCAIChat o CCAIScreenShare).

Per integrare l'SDK per iOS nella tua app, segui la sequenza di passaggi successiva.

  1. Aggiungi quanto segue al tuo file Package.swift:

    dependencies: [
       .package(url: "https://github.com/UJET/ccai-ios-sdk.git", from: "3.3.1")
    ],
    targets: [
       .target(
           name: "YourTargetName",
           dependencies: [
               .product(name: "CCAIKit", package: "CCAIKit")
           ]
       )
    ]
    
  2. Importa CCAIKit nel file AppDelegate:

    import CCAIKit
    
  3. Se il tuo progetto è SwiftUI, dichiara @UIApplicationDelegateAdaptor nella struttura App:

    @main
    struct YourAppName: App {
       @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
       // ...
    }
    

Inizializza l'SDK headless

Inizializza l'SDK all'avvio dell'app. A causa dell'architettura modulare dell'SDK, devi inizializzare prima il sistema CCAI principale e poi registrare i moduli di servizio specifici (come la chat o la condivisione schermo).

Inizializzazione di base

L'SDK utilizza una struttura InitOptions che raggruppa tutta la configurazione richiesta:

import CCAIKit
import CCAIChat       // If using Chat
import CCAIScreenShare // If using Screen Share

class AppDelegate: NSObject, UIApplicationDelegate, CCAIDelegate {

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

        // 1. Configure initialization options
        let options = InitOptions(
            key: "YOUR_COMPANY_KEY",
            urlHost: "your_subdomain.ccaiplatform.com",
            delegate: self,
            languageCode: "en",       // ISO 639 code (defaults to "en")
            cacheAuthToken: true       // Whether to cache the auth token (defaults to true)
        )

        // 2. Initialize the core SDK
        do {
            try CCAI.shared.initialize(options: options)
        } catch {
            print("Failed to initialize CCAI: \(error.localizedDescription)")
            return true
        }

        // 3. Initialize feature modules
        // Chat minimal (no options required)
        CCAI.shared.initializeChat()

        // Chat with optional configuration
        // let chatOptions = ChatOptions(
        //     delegate: webFormDelegate,           // Implement to intercept and render custom web forms within your own UI
        //     downloadTranscriptVisibility: .hideInPostChat, // Hide the post-chat download affordance
        //     greeting: "Hello! How can I help you?"
        // )
        // CCAI.shared.initializeChat(chatOptions)

        // Screen Share (optional)
        // let screenShareOptions = ScreenShareOptions(
        //     key: "YOUR_SCREEN_SHARE_KEY",
        //     domain: "your_domain.com"
        // )
        // CCAI.shared.initializeScreenShare(screenShareOptions)

        return true
    }

    // ... CCAIDelegate methods (Authentication) go here ...
}

Riferimento alla configurazione di InitOptions

public struct InitOptions {
    /// The company key used for authentication
    public let key: String

    /// The host URL for CCAI platform (for example, "my-unique-instance.uc1.ccaiplatform.com")
    public let urlHost: String

    /// The preferred language code for localization (defaults to "en")
    public var languageCode: String

    /// The delegate that receives SDK events and callbacks
    public weak var delegate: CCAIDelegate?

    /// Whether to cache the authentication token (defaults to true)
    public let cacheAuthToken: Bool
}

Servizi SDK disponibili

Dopo l'inizializzazione, puoi accedere a vari servizi tramite il singleton CCAI.shared:

let authService = CCAI.shared.authService
let companyService = CCAI.shared.companyService
let queueMenuService = CCAI.shared.queueMenuService
let optionsService = CCAI.shared.optionsService
let languageService = CCAI.shared.languageService
let pushNotificationService = CCAI.shared.pushNotificationService
let endUserService = CCAI.shared.endUserService
let chatService = CCAI.shared.chatService           // Only available after initializeChat()
let screenShareService = CCAI.shared.screenShareService // Only available after initializeScreenShare()
let rateService = CCAI.shared.rateService            // For CSAT ratings and survey submission
let smartActionService = CCAI.shared.smartActionService // Dedicated service for smart action lifecycle

Recupera la configurazione aziendale

Dopo l'inizializzazione, recupera la configurazione a livello aziendale utilizzando companyService. È utile per compilare i selettori di lingua, visualizzare il nome dell'azienda o leggere i dati di contatto dell'assistenza prima che l'utente entri in una coda.

do {
    let company = try await CCAI.shared.companyService?.get()
    print("Company: \(company?.displayName ?? "Unknown")")
} catch {
    print("Failed to get company info: \(error)")
}

Autenticazione degli utenti finali

L'SDK mobile headless utilizza i token web JSON (JWT) per autenticare gli utenti e trasferire in modo sicuro le informazioni contestuali al CRM dell'agente.

Come funziona

L'SDK utilizza un flusso di autenticazione asincrona semplificato in due passaggi:

  1. L'SDK determina che è necessario autenticare l'utente.

  2. Chiama il metodo CCAIDelegate, ccaiShouldAuthenticate(), che è una funzione asincrona.

  3. La tua app firma un JWT in remoto sul server di backend utilizzando il codice segreto della tua azienda.

  4. La tua app passa quindi il JWT firmato a CCAI.shared.authService?.authenticate(jwt) per scambiarlo con un token di autenticazione.

  5. Il token di autenticazione viene restituito all'SDK per completare la connessione.

Implementare CCAIDelegate per l'autenticazione

La tua app deve implementare il protocollo CCAIDelegate. L'SDK chiama il metodo delegato asincrono singolo quando richiede l'autenticazione.

Protocollo CCAIDelegate

public protocol CCAIDelegate: AnyObject {
    /// Authenticates the end user and returns an auth token
    func ccaiShouldAuthenticate() async -> String?
}

Esempio di implementazione (Swift)

class AppDelegate: CCAIDelegate {
    func ccaiShouldAuthenticate() async -> String? {
        // 1. Sign JWT remotely on your backend server
        guard let jwt = await signJWTRemotely() else { return nil }

        // 2. Authenticate JWT using authService to get an auth token
        return try? await CCAI.shared.authService?.authenticate(jwt)
    }
}

Esempio completo con firma JWT per riferimento e test

class AuthController: CCAIDelegate {
    func ccaiShouldAuthenticate() async -> String? {
        var jwt = JWT(claims: claims)

        // First, sign JWT with company secret (do this on your backend in production)
        guard let secret = self.companySecret,
              let key = secret.data(using: .utf8) else { return nil }
        let signer = JWTSigner.hs384(key: key)
        guard let encodedJWT = try? jwt.sign(using: signer) else { return nil }

        // Then authenticate JWT using authService to get an auth token
        return try? await CCAI.shared.authService?.authenticate(encodedJWT)
    }
}

Trasferire dati personalizzati al CRM

Se vuoi trasmettere dati contestuali all'agente (ad esempio, il sistema operativo del dispositivo attuale dell'utente, la sua posizione o il suo livello di account), il backend deve inserire un oggetto custom_data nel payload JWT prima di firmarlo.

Ogni dato personalizzato deve essere formattato come oggetto JSON contenente un label (ciò che vede l'agente), un value e un type.

Tipi di dati supportati

  • string: testo standard (ad esempio, "iPhone 14 Pro").

  • number: numeri interi o in virgola mobile (ad esempio, 1234 o 99,99).

  • date: un timestamp Unix UTC a 13 cifre che include i millisecondi (ad esempio, 1537399655992).

  • url: formato URL HTTP/HTTPS standard.

  • boolean: valore vero o falso standard.

Chiavi CRM riservate

La piattaforma CCAI supporta chiavi riservate specifiche che attivano comportamenti integrati nella piattaforma, ad esempio contrassegnare un utente come VIP o avvisare un agente di un malintenzionato. Questi devono essere formattati come tipi booleani e vengono accettati solo se il payload è firmato utilizzando il metodo JWT sicuro.

  • reserved_verified_customer: indica se un cliente è stato autenticato correttamente dai tuoi sistemi interni.

  • reserved_bad_actor: segnala l'utente all'agente come potenziale spammer o account fraudolento.

  • reserved_repeat_customer: Indica se questo cliente ha contattato di frequente l'assistenza di recente.

Esempio di payload con chiavi riservate

{
  "iat": 1537399656,
  "exp": 1537400256,
  "custom_data": {
    "reserved_verified_customer": {
      "label": "Verified Customer",
      "value": true,
      "type": "boolean"
    },
    "reserved_bad_actor": {
      "label": "Bad Actor",
      "value": false,
      "type": "boolean"
    },
    "reserved_repeat_customer": {
      "label": "Repeat Customer",
      "value": false,
      "type": "boolean"
    }
  }
}

Gestisci i token di autenticazione

L'SDK fornisce anche metodi per aggiornare o cancellare manualmente il token di autenticazione memorizzato nella cache:

// Set a new auth token
CCAI.shared.authService?.updateAuthToken("new_auth_token")

// Clear the current token (for example, on user logout)
CCAI.shared.authService?.updateAuthToken(nil)

Schema del payload JWT dei dati personalizzati

Quando il backend crea il payload finale da firmare con il codice segreto della tua azienda, deve seguire rigorosamente questo schema. Prendi nota dei timestamp obbligatori iat (data di emissione) e exp (scadenza).

{
  "iat": 1537399656,
  "exp": 1537400256,
  "custom_data": {
    "os_version": {
      "label": "OS Version",
      "value": "16.4",
      "type": "string"
    },
    "membership_tier": {
      "label": "Membership Tier",
      "value": "Platinum",
      "type": "string"
    },
    "ssn_last_four": {
      "label": "SSN",
      "value": "1234",
      "type": "string",
      "invisible_to_agent": true
    },
    "reserved_verified_customer": {
      "label": "Verified Customer",
      "value": true,
      "type": "boolean"
    }
  }
}

Attivare le notifiche push

L'SDK utilizza le notifiche push per:

  • Chiamate in arrivo.

  • Determinate azioni rapide ed eventi correlati alle chiamate.

  • Mantenere lo stato quando l'app è in background.

Configurare i certificati

CCAI Platform richiede il salvataggio di due diversi tipi di certificati push Apple nel portale amministratore di CCAI Platform:

  • Un certificato SSL del servizio di notifiche push di Apple (APNs) utilizzato per le notifiche push remote standard.

  • Un certificato di servizi VoIP, utilizzato per le notifiche di chiamata in arrivo e per determinate azioni intelligenti correlate alle chiamate.

Entrambi i certificati vengono creati nel portale Apple Developer, convertiti in formato PEM e poi caricati nel portale di amministrazione di CCAI Platform.

Crea un certificato SSL APNs

Segui questi passaggi successivi, basati sulla guida ufficiale di Apple Establishing a Certificate-Based Connection to APNs, per creare un certificato SSL APNs.

  1. Accedi a developer.apple.com e vai a Certificati, identificatori e profili.

  2. Seleziona il pulsante + per creare un nuovo certificato.

  3. In Servizi, seleziona SSL del servizio di notifiche push di Apple (sandbox e produzione).

  4. Scegli l'ID app che corrisponde all'identificatore pacchetto della tua applicazione, poi seleziona Continua.

  5. Carica una richiesta di firma del certificato (CSR) generata da Accesso Portachiavi sul tuo Mac:

    • Apri Accesso Portachiavi > Assistente certificati > Richiedi un certificato da un'autorità di certificazione…

    • Inserisci il tuo indirizzo email, lascia vuoto il campo Indirizzo email CA, seleziona Salva su disco e seleziona Continua.

  6. Seleziona Continua, poi Scarica il file .cer generato.

  7. Fai doppio clic sul file .cer per installarlo nel tuo portachiavi.

  8. In Accesso Portachiavi, individua il certificato installato, fai clic con il tasto destro del mouse e seleziona Esporta… per salvarlo come file .p12. Ti viene chiesto di impostare una password di esportazione.

  9. Converti il file .p12 in formato PEM utilizzando openssl:

    # Extract the certificate
    openssl pkcs12 -in apns_certificate.p12 -out apns_cert.pem -clcerts -nokeys
    
    # Extract the private key
    openssl pkcs12 -in apns_certificate.p12 -out apns_key.pem -nocerts -nodes
    
    # (Optional) Combine into a single PEM file
    cat apns_cert.pem apns_key.pem > apns_combined.pem
    

Crea un certificato per i servizi VoIP

I certificati dei servizi VoIP consentono alla tua app di ricevere notifiche push con priorità elevata tramite il framework PushKit di Apple, che può riattivare l'app dallo stato di chiusura per gestire le chiamate in arrivo. Per i passaggi ufficiali più recenti, consulta Creare certificati per i servizi VoIP nella documentazione per sviluppatori Apple.

  1. Accedi a developer.apple.com e vai a Certificati, identificatori e profili.

  2. Seleziona il pulsante + per creare un nuovo certificato.

  3. Nella sezione Servizi, seleziona Certificato servizi VoIP.

  4. Scegli l'ID app che corrisponde all'identificatore pacchetto della tua applicazione, poi seleziona Continua.

  5. Carica la stessa richiesta di firma del certificato (CSR) che hai generato per il tuo certificato SSL APN (o creane una nuova).

  6. Seleziona Continua, poi Scarica il file .cer generato.

  7. Fai doppio clic sul file .cer per installarlo nel tuo portachiavi.

  8. In Accesso Portachiavi, individua il certificato dei servizi VoIP installati, fai clic con il tasto destro del mouse e seleziona Esporta… per salvarlo come file .p12.

  9. Converti il file .p12 in formato PEM utilizzando openssl:

    # Extract the certificate
    openssl pkcs12 -in voip_certificate.p12 -out voip_cert.pem -clcerts -nokeys
    
    # Extract the private key
    openssl pkcs12 -in voip_certificate.p12 -out voip_key.pem -nocerts -nodes
    
    # (Optional) Combine into a single PEM file
    cat voip_cert.pem voip_key.pem > voip_combined.pem
    

Carica i certificati nel portale amministratore di CCAI Platform

  1. Accedi al portale amministratore di CCAI Platform.

  2. Vai a Impostazioni > Impostazioni sviluppatore > App mobile.

  3. Carica il certificato PEM del servizio APN.

  4. Carica il certificato PEM VoIP.

Richiedere le autorizzazioni alle notifiche push

L'SDK fornisce un metodo pratico per richiedere l'autorizzazione delle notifiche push (solo iOS):

let options: UNAuthorizationOptions = [.alert, .sound]
try await CCAI.shared.pushNotificationService?.registerForPushNotifications(
    options: options
) { granted in
    if granted {
        print("Push notifications granted")
    } else {
        print("Push notifications denied")
    }
}

Registrarsi per le notifiche da remoto

Implementa i delegati delle notifiche push in AppDelegate. L'SDK fornisce pushNotificationService per l'inoltro dei token:

import CCAIKit

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        CCAI.shared.pushNotificationService?.updatePushToken(
            data: deviceToken,
            type: .apns
        )
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        CCAI.shared.pushNotificationService?.updatePushToken(
            data: nil,
            type: .apns
        )
    }
}

Assicurati di attivare anche Funzionalità > Notifiche push e Modalità in background per le notifiche remote dalla scheda Target > Firma e funzionalità in Xcode.

Gestire le notifiche push in arrivo

Quando ricevi un payload push, inoltralo all'SDK per l'elaborazione:

func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    // Let the SDK determine if this push relates to an incoming call,
    // session update, or another internal event
    CCAI.shared.pushNotificationService?.handlePushNotification(userInfo)
    completionHandler(.newData)
}

L'SDK determina se la notifica push riguarda una chiamata in arrivo, un aggiornamento della sessione o un altro evento interno e la gestisce di conseguenza.

Registrarsi per le notifiche push VoIP

Per le chiamate in entrata e alcune azioni intelligenti basate su PSTN, l'SDK richiede un token push VoIP oltre al token APN standard. Le notifiche VoIP vengono inviate con priorità elevata e possono riattivare l'app dallo stato di chiusura forzata.

Importa PushKit e crea un PKPushRegistry nel didFinishLaunchingWithOptions del tuo AppDelegate

import PushKit

class AppDelegate: NSObject, UIApplicationDelegate, PKPushRegistryDelegate {

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

        // ... SDK initialization ...

        // Register for VoIP push notifications
        let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [.voIP]

        return true
    }
}

Implementa i metodi PKPushRegistryDelegate per inoltrare token e payload VoIP all'SDK

// MARK: - PKPushRegistryDelegate

func pushRegistry(
    _ registry: PKPushRegistry,
    didUpdate credentials: PKPushCredentials,
    for type: PKPushType
) {
    // Forward the VoIP token to the SDK
    CCAI.shared.pushNotificationService?.updatePushToken(
        data: credentials.token,
        type: .voip
    )
}

func pushRegistry(
    _ registry: PKPushRegistry,
    didInvalidatePushTokenFor type: PKPushType
) {
    // Clear the VoIP token
    CCAI.shared.pushNotificationService?.updatePushToken(
        data: nil,
        type: .voip
    )
}

func pushRegistry(
    _ registry: PKPushRegistry,
    didReceiveIncomingPushWith payload: PKPushPayload,
    for type: PKPushType,
    completion: @escaping () -> Void
) {
    guard type == .voIP else {
        completion()
        return
    }

    // Forward the VoIP payload to the SDK using the `handleVoIPPush`
    // extension on `CCAI` (defined in the `CCAICall` module). The SDK
    // takes ownership of the call lifecycle; invoke PushKit's completion
    // handler in the trailing closure after the SDK has accepted the
    // payload.
    CCAI.shared.handleVoIPPush(payload: payload.dictionaryPayload) { completion() }
}