SDK mobile headless per iOS: voce, condivisione dello schermo, chiamata pianificata ed email

Questo documento spiega come integrare e personalizzare l'SDK mobile headless nella tua app per Android. Copre le chiamate vocali tramite CCAICall

Chiamate vocali

L'SDK mobile headless per iOS fornisce il supporto delle chiamate vocali tramite il modulo CCAICall. La tua app è proprietaria dell'interfaccia utente di chiamata personalizzata, mentre l'SDK gestisce la connessione della chiamata, l'integrazione di CallKit, la gestione delle notifiche push VoIP e gli eventi del ciclo di vita della chiamata.

Cosa fornisce l'SDK: API di servizi di chiamata per avviare chiamate istantanee, avviare la segreteria, gestire le chiamate in arrivo e osservare lo stato delle chiamate.

Cosa crei: l'esperienza di chiamata visiva, inclusi il punto di ingresso pre-chiamata, la richiesta di consenso alla registrazione, gli stati di chiamata o attesa, i controlli durante la chiamata e qualsiasi navigazione post-chiamata.

Inizializza il modulo di chiamata

Inizializza il modulo di chiamata dopo aver inizializzato l'SDK principale:

import CCAIKit
import CCAICall

try CCAI.shared.initialize(options: options)
CCAI.shared.initializeCall(CallOptions())

Ispezione delle funzionalità di VoiceCallChannel

Prima di offrire una chiamata immediata, esamina la struttura VoiceCallChannel della coda per decidere quali punti di ingresso esporre (chiamata immediata, fallback della segreteria telefonica, deviazione a livello di canale) e se è necessario il consenso alla registrazione.

Campi chiave VoiceCallChannel

  • instantEnabled: Bool?: indica se è consentita una chiamata vocale live (istantanea) per questa coda. Considera nil o false come stato "don't offer an instant call button". A differenza di voiceCall != nil, che ti dice solo se la coda ha un canale vocale.

  • preSessionSmartAction: Bool?: indica se sono richieste azioni intelligenti pre-sessione su questo canale specifico prima di avviare una chiamata. Consulta SDK mobile headless per iOS: azioni intelligenti, allegati e deviazione per il percorso complementare a livello di coda.

  • scheduleEnabled: Bool?: indica se le chiamate pianificate sono supportate. Consulta Chiamate pianificate per il percorso complementare a livello di coda.

  • recordingOption: RecordingOption?: il modello di consenso per la registrazione della chiamata (vedi Consenso alla registrazione). A differenza di RecordingPermission.notAsked, che è specifico per la pianificazione.

  • voicemailReason: String?: compilato quando il canale è configurato per il routing alla segreteria.

  • phoneNumber: String?: numero di fallback RTG configurato per il trasferimento o la segreteria.

  • scheduleDeflectionType: String? / deflected: Bool? / deflectedReason: String?: metadati di deviazione a livello di canale. Quando deflected == true, mostra il messaggio deflectedReason configurato dall'amministratore e offri il fallback configurato (ad esempio, un numero di telefono da chiamare o un punto di ingresso per le chiamate pianificate) anziché tentare una chiamata immediata. Consulta Deviazione (al di fuori dell'orario di lavoro e in caso di capacità eccessiva) per la gestione della deviazione a livello di coda.

func voiceEntryPoints(for menu: QueueMenu) {
    guard let voice = menu.channels?.voiceCall else {
        hideCallNowButton()
        hideVoicemailButton()
        return
    }

    // Instant call - only when explicitly enabled and not deflected
    callNowButton.isHidden = (voice.instantEnabled != true) || (voice.deflected == true)

    // Voicemail fallback
    if let number = voice.phoneNumber, voice.voicemailReason != nil {
        showVoicemailButton(number: number, reason: voice.voicemailReason)
    }

    // Channel-level deflection
    if voice.deflected == true {
        showChannelDeflection(reason: voice.deflectedReason)
    }
}

Cosa fornisce l'SDK: un campo recordingOption: RecordingOption? su VoiceCallChannel che descrive il modello di consenso alla registrazione delle chiamate configurato dagli amministratori nel portale.

Cosa crei: la richiesta di consenso (quando .askUser) e la logica di ramificazione che rispetta la scelta dell'amministratore.

public enum RecordingOption: String, Decodable {
    case always   // Calls are always recorded - surface a disclosure before connecting
    case never    // Calls are never recorded - no disclosure needed
    case askUser  // Prompt the user for consent before the call connects
}

func handleRecordingConsent(
    for voice: VoiceCallChannel,
    completion: @escaping (Bool) -> Void
) {
    switch voice.recordingOption {
    case .always:
        showRecordingDisclosure(message: "This call will be recorded.")
        completion(true)
    case .never, .none:
        completion(true)
    case .askUser:
        presentConsentPrompt { granted in
            completion(granted)
        }
    }
}

Avviare una chiamata istantanea

Prima di avviare una chiamata istantanea:

  1. Esamina VoiceCallChannel della coda selezionata.

  2. Verifica che le chiamate istantanee siano attive e non deviate.

  3. Gestisci il consenso alla registrazione in base ai recordingOption della coda.

  4. Richiedi l'autorizzazione microfono.

  5. Chiama il startInstantCall.

func initiateInstantCall(for menu: QueueMenu) async {
    guard let voice = menu.channels?.voiceCall,
          voice.instantEnabled == true,
          voice.deflected != true else {
        return
    }

    let recordingPermission = await resolveRecordingPermission(for: voice)
    guard await requestMicrophonePermission() else { return }

    do {
        try await CCAI.shared.callService?.startInstantCall(
            InstantCallRequest(
                menuId: menu.id,
                recordingPermission: recordingPermission
            )
        )
    } catch {
        showCallStartError(error)
    }
}

Osservare lo stato della chiamata

Iscriviti agli eventi di servizio di chiamata prima di avviare o accettare una chiamata:

CCAI.shared.callService?.stateChanged
    .receive(on: DispatchQueue.main)
    .sink { state in
        updateCallConnectionState(state)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.callReceived
    .receive(on: DispatchQueue.main)
    .sink { call in
        updateCallDetails(call)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.incomingCallEvent
    .receive(on: DispatchQueue.main)
    .sink { event in
        handleIncomingCallEvent(event)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.waitTimeUpdated
    .receive(on: DispatchQueue.main)
    .sink { waitTime in
        // The platform pushed a new estimated wait time for the active call.
        updateEstimatedWaitTime(waitTime)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.participantUpdated
    .receive(on: DispatchQueue.main)
    .sink { participant in
        // Agent assignment, transfer, or participant metadata changed.
        updateParticipantInfo(participant)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.interruptionDetected
    .receive(on: DispatchQueue.main)
    .sink { interruption in
        // A PSTN interruption (for example, an incoming cellular call)
        // has paused the active CCAI call. Surface a "call interrupted"
        // banner and offer a Resume action after the interruption ends.
        showInterruptionBanner(reason: interruption)
    }
    .store(in: &cancellables)

CCAI.shared.callService?.deflectionOffered
    .receive(on: DispatchQueue.main)
    .sink { deflection in
        // The platform offered a wait-time deflection (for example, a
        // scheduled callback or PSTN fallback). Render the prompt and
        // honor the user's choice.
        presentWaitTimeDeflection(deflection)
    }
    .store(in: &cancellables)

Stato del provider e stato del server

stateChanged e callReceived.status sono indicatori indipendenti: leggili come colonne, non righe. stateChanged del provider segnala l'handshake VoIP locale; callReceived.status lato server segnala il ciclo di vita della chiamata sulla piattaforma. Guida il flip Connesso e l'avvio del timer di chiamata da callReceived.status, non da stateChanged che raggiunge .connected.

Indicatore Cosa viene segnalato Cosa dedurre
stateChanged (fornitore) Handshake VoIP locale: .connecting, .connected (locale), .disconnected, .error(...). (Facoltativo) Indicatore di caricamento pre-stato prima dell'arrivo dello stato del server, della visualizzazione dell'errore e della pulizia di fine chiamata.
callReceived.status (server) Ciclo di vita lato server: .waiting, .connecting, .connected, .onHold, .interrupted, .ended. Connessione, avvio del timer di chiamata, UI assegnata all'agente, overlay di trasferimento o riassegnazione e flusso di fine chiamata.

Stato della chiamata e stati lato server

Cosa fornisce l'SDK: un enum CallStatus basato sul server in ogni payload Call che descrive a che punto del ciclo di vita si trova la chiamata (ad esempio, connecting, waiting, connected, onHold, interrupted, ended).

Cosa crei: la mappatura da CallStatus agli stati visibili ai clienti nella schermata di chiamata (squillo, in attesa in coda, connessa, in attesa, interrotta, terminata).

Esempio di implementazione

Attiva call.status da ogni emissione callReceived e visualizza lo stato dell'interfaccia utente corrispondente.

func updateCallDetails(_ call: Call) {
    switch call.status {
    case .connecting:
        renderConnectingState()
    case .waiting:
        renderWaitingInQueueState(estimatedWait: call.estimatedWait)
    case .connected:
        renderConnectedState(agent: call.agent)
    case .onHold:
        renderOnHoldState()
    case .interrupted:
        renderInterruptedState()
    case .ended, .failed:
        renderEndedState(reason: call.endReason)
    @unknown default:
        break
    }
}

Controlli durante la chiamata (disattivazione audio, altoparlante, attesa)

Cosa fornisce l'SDK: proprietà di sola lettura su callService: isMuted, isSpeakerEnabled, isOnHold, insieme alle azioni per attivare/disattivare ogni stato (setMuted, setSpeakerEnabled, setOnHold).

Cosa crei: i pulsanti Muto, Altoparlante e Attesa nell'interfaccia utente durante la chiamata, collegati alle azioni dell'SDK e che riflettono lo stato attuale.

Esempio di implementazione

Leggi lo stato attuale da callService per inizializzare i pulsanti, poi attiva/disattiva e aggiorna con un tocco.

// Read current state
muteButton.isSelected = CCAI.shared.callService?.isMuted ?? false
speakerButton.isSelected = CCAI.shared.callService?.isSpeakerEnabled ?? false
holdButton.isSelected = CCAI.shared.callService?.isOnHold ?? false

// Toggle from your UI handlers
func onMuteTapped() {
    let next = !(CCAI.shared.callService?.isMuted ?? false)
    CCAI.shared.callService?.setMuted(next)
    muteButton.isSelected = next
}

func onSpeakerTapped() {
    let next = !(CCAI.shared.callService?.isSpeakerEnabled ?? false)
    CCAI.shared.callService?.setSpeakerEnabled(next)
    speakerButton.isSelected = next
}

func onHoldTapped() {
    let next = !(CCAI.shared.callService?.isOnHold ?? false)
    CCAI.shared.callService?.setOnHold(next)
    holdButton.isSelected = next
}

Riprendere una chiamata in corso dopo un'interruzione

Quando un'interruzione della rete PSTN (ad esempio una chiamata cellulare in arrivo) mette in pausa la chiamata CCAI attiva, l'SDK mantiene attiva la sessione ed emette interruptionDetected. Al termine dell'interruzione, l'app può riprendere la chiamata.

Cosa fornisce l'SDK: getLastCallInProgress() per cercare una chiamata in corso (anche dopo il riavvio dell'app) e resumeCall(_:) per riportarla in primo piano.

Cosa crei: la funzionalità di ripresa nell'interfaccia utente (in genere un banner o una ripresa automatica al termine dell'interruzione) e la navigazione per tornare alla schermata della chiamata.

Esempio di implementazione

Cerca la chiamata in corso con getLastCallInProgress, riprendila e torna alla schermata della chiamata.

func resumeIfInterrupted() async {
    guard let call = try? await CCAI.shared.callService?.getLastCallInProgress() else {
        return
    }
    do {
        try await CCAI.shared.callService?.resumeCall(call)
        navigateToCallScreen(call)
    } catch {
        showCallResumeError(error)
    }
}

Riassegnazione da un agente virtuale

Cosa fornisce l'SDK: canEscalate(allowSkipVirtualAgent:) è un metodo su CallResponse (il modello emesso da callService.callReceived), non su callService stesso. Indica se la chiamata attuale è idonea per il trasferimento a un operatore umano in base allo stato attuale dell'agente virtuale della chiamata.

Cosa crei: mantieni l'emissione callReceived più recente come stato "current call", poi associa la visibilità del pulsante Parla con una persona a currentCall?.canEscalate(...). L'argomento allowSkipVirtualAgent è un valore di configurazione del menu che recuperi separatamente dalla risposta alla chiamata stessa. Non codificare true in produzione.

Esempio di implementazione

Conserva l'ultimo Call in una proprietà memorizzata, poi richiama canEscalate. Risolvi allowSkipVirtualAgent dalla configurazione del menu.

// Held from the most recent `callReceived` emission
private var currentCall: Call?

// `allowSkipVirtualAgent` is fetched separately from menu configuration
// it is not derived from the CallResponse.
let allowSkip = menuConfiguration.allowSkipVirtualAgent
let allowed = currentCall?.canEscalate(allowSkipVirtualAgent: allowSkip) ?? false
talkToAgentButton.isHidden = !allowed

Segreteria

Cosa fornisce l'SDK: startVoicemail(_ request: VoicemailRequest) su callService, oltre a un'enumerazione VoicemailReason che descrive il motivo per cui la piattaforma ha offerto la segreteria vocale in primo luogo (ad esempio, fallback dopo l'orario di lavoro, fallback per capacità eccessiva o reindirizzamento temporaneo).

Cosa crei: il punto di accesso alla segreteria nella tua UI (in genere mostrato quando VoiceCallChannel.voicemailReason è compilato o quando il ciclo di vita della chiamata si risolve in un risultato di segreteria) e l'UI di registrazione stessa.

Esempio di implementazione

Mostra il pulsante della segreteria quando è impostato voicemailReason, quindi chiama startVoicemail con un VoicemailRequest.

public enum VoicemailReason: String, Decodable {
    case afterHourDeflection
    case overCapacityDeflection
    case temporaryRedirection
}

func offerVoicemail(for menu: QueueMenu) async {
    guard let voice = menu.channels?.voiceCall,
          voice.voicemailReason != nil else {
        return
    }
    do {
        try await CCAI.shared.callService?.startVoicemail(
            VoicemailRequest(menuId: menu.id)
        )
    } catch {
        showVoicemailError(error)
    }
}

Riduzione dei tempi di attesa

Quando la coda è aperta, ma il tempo di attesa supera la soglia configurata dall'amministratore, la piattaforma può offrire un percorso alternativo, in genere un richiamo programmato o un trasferimento alla rete PSTN.

Cosa fornisce l'SDK: un payload CallDeflection che descrive l'alternativa offerta, recuperabile utilizzando getCallDeflection(callId:) su callService per la chiamata attiva. La piattaforma emette lo stesso payload anche tramite l'editore deflectionOffered quando una deviazione diventa disponibile a metà attesa. Un flag CompanyResponse.preventDirectPstnCall separato indica se la composizione diretta PSTN deve essere nascosta nell'UI di fallback.

Cosa crei: il prompt di deviazione (ad esempio, L'attesa stimata è di 12 minuti. Vuoi programmare una richiamata?) e la logica di ramificazione che rispetta la scelta dell'utente.

Esempio di implementazione

Cerca la chiamata attiva con getLastCallInProgress, recupera il trasferimento offerto con getCallDeflection(callId:), combinalo con CompanyResponse.preventDirectPstnCall e presenta il prompt.

func evaluateWaitTimeDeflection() async {
    guard let currentCall = try? await CCAI.shared.callService?.getLastCallInProgress() else {
        return
    }
    let deflection = try? await CCAI.shared.callService?.getCallDeflection(
        callId: currentCall.id
    )
    guard let offer = deflection else { return }
    let company = try? await CCAI.shared.companyService?.get()
    let allowDirectPstn = company?.preventDirectPstnCall != true
    presentWaitTimeDeflection(offer, allowDirectPstn: allowDirectPstn)
}

Messaggi sonori (annunci di sistema)

Cosa fornisce l'SDK: un audibleMessageService che risolve i messaggi udibili configurati dall'amministratore per una coda (ad esempio, Tutti gli agenti sono occupati). Per impostazione predefinita, l'SDK riproduce automaticamente questi messaggi quando la chiamata entra nello stato corrispondente.

Cosa crei: nulla, nella modalità predefinita. Se preferisci eseguire il rendering dell'annuncio autonomamente (ad esempio, come testo dei sottotitoli codificati più il tuo lettore audio), inizializza il modulo di chiamata con playAudibleMessages = false e chiama resolveMessages(menuId:) direttamente.

Esempio di implementazione

Utilizza l'inizializzatore predefinito per il percorso riprodotto dall'SDK o imposta playAudibleMessages = false e risolvi i messaggi autonomamente per visualizzare i sottotitoli codificati e riprodurre l'audio nella tua UI.

// Option A default: SDK plays audible messages automatically.
CCAI.shared.initializeCall(CallOptions())

// Option B your app renders the announcements manually.
CCAI.shared.initializeCall(CallOptions(playAudibleMessages: false))
let messages = try await CCAI.shared.audibleMessageService?.resolveMessages(
    menuId: menu.id
)
for message in messages ?? [] {
    renderCaption(message.text)
    playAudio(message.audioURL)
}

Avanzata CallOptions

CallOptions espone un piccolo insieme di pulsanti di attivazione/disattivazione per le app che devono disattivare l'esperienza di chiamata predefinita.

  • playAudibleMessages: Bool: quando false, l'SDK non riproduce gli annunci di sistema; esegui il rendering autonomamente utilizzando audibleMessageService.

  • callKitEnabled: Bool: quando false, l'SDK non presenta la schermata CallKit integrata per le chiamate in entrata o in uscita. Utilizza questa opzione quando la tua app deve eseguire il rendering della propria UI per le chiamate in arrivo in-app anziché fare affidamento sulla UI di sistema di iOS.

Esempio di implementazione

Passa i pulsanti di attivazione/disattivazione a CallOptions durante l'inizializzazione del modulo di chiamata.

// Fully custom in-app call experience
CCAI.shared.initializeCall(
    CallOptions(
        playAudibleMessages: false,
        callKitEnabled: false
    )
)

Condivisione schermo

La condivisione dello schermo consente agli agenti dell'assistenza di visualizzare lo schermo dell'utente per risolvere i problemi.Il progetto Google Cloud supporta due modalità di condivisione dello schermo:

  • Condivisione in-app: l'agente può vedere solo cosa succede all'interno della tua app specifica.

  • Condivisione dell'intero dispositivo (facoltativa): l'agente può vedere l'intero dispositivo dell'utente, inclusa la schermata Home di iOS e altre app. Questa operazione richiede una configurazione avanzata di iOS utilizzando ReplayKit di Apple.

Cosa fornisce l'SDK: il servizio di condivisione dello schermo che crea e gestisce sessioni, gestisce le modifiche di stato ed elabora le richieste di consenso.

Cosa crei: la configurazione di inizializzazione, le richieste della UI per il consenso dell'utente e (se supporti l'intero dispositivo) l'estensione di trasmissione iOS integrata.

Il servizio di condivisione dello schermo fornisce metodi per l'intero ciclo di vita della sessione: create, activate, end, clear (solo iOS), oltre a metodi di configurazione updateOptions, enableRemoteControl e enableFullDeviceSharing. Le sottosezioni successive illustrano il flusso di integrazione principale.

Per le firme complete dei metodi e i dettagli dei parametri per tutti i metodi di condivisione dello schermo, consulta il Riferimento API dell'SDK mobile headless per iOS.

Inizializzare e configurare la condivisione schermo

Prima che un agente possa richiedere la condivisione dello schermo, devi inizializzare e configurare il servizio con il codice licenza specifico per la condivisione dello schermo.

import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaikit.initializeScreenShare
import com.ccaiplatform.ccaikit.models.screenShare.ScreenShareOptions

// Minimal domain defaults to your instance's configured provider
CCAI.initializeScreenShare(
    context = this,
    options = ScreenShareOptions(
        key = "YOUR_COMPANY_KEY"
    )
)

// If your instance or provider requires an explicit domain:
// CCAI.initializeScreenShare(
//     context = this,
//     options = ScreenShareOptions(
//         key = "YOUR_COMPANY_KEY",
//         domain = "your_subdomain.ccaiplatform.com"
//     )
// )

Verifica l'idoneità alla condivisione dello schermo

Prima di attivare la funzionalità di condivisione schermo, verifica se è disponibile per la chat corrente:

let isChatSupportingScreenShare = CCAI.shared.screenShareService != nil
    && chat?.isSupportingScreenShare == true

Gestire le richieste di condivisione schermo

Implementa la gestione delle richieste di condivisione schermo utilizzando screenShareService. Crea un ScreenShareRequest e gestisci la risposta asincrona:

func requestScreenShare(isFromRemote: Bool) async {
    guard let chatId = currentChatId else { return }
    guard let screenShareService = CCAI.shared.screenShareService else {
        handleError(ScreenShareError.serviceNotAvailable, message: "Request Screen Share")
        return
    }

    // Track whether this session is agent-initiated
    screenShareInitiatedFrom = isFromRemote ? .agent : .endUser

    if !isFromRemote {
        await sendScreenShareMessage(event: .screenShareRequestedFromEndUser)
    }

    do {
        let response = try await screenShareService.create(
            request: ScreenShareRequest(
                communicationId: chatId,
                communicationType: .chat,
                initiatedFrom: isFromRemote ? .agent : .endUser
            )
        )
        await sendScreenShareMessage(event: .screenShareCodeGenerated, response: response)
    } catch {
        await sendScreenShareMessage(event: .screenShareFailed)
        handleError(error, message: "Request screen share error")
    }
}

Gestire le modifiche dello stato di condivisione schermo

Osserva lo stato e le richieste di condivisione dello schermo tramite i publisher di servizi. Iscriviti prima di creare o attivare una sessione in modo che la tua UI possa rispondere a ogni transizione.

CCAI.shared.screenShareService?.statePublisher
    .receive(on: DispatchQueue.main)
    .sink { state in
        currentScreenShareSessionState = state
        if state == .authorizing, screenShareInitiatedFrom == .agent {
            Task {
                do {
                    try await handleStartSession(isAccepted: true)
                } catch {
                    handleError(error, message: "Failed to start screen share")
                }
            }
        }
    }
    .store(in: &cancellables)

CCAI.shared.screenShareService?.requestPublisher
    .receive(on: DispatchQueue.main)
    .sink { request in
        switch request {
        case .session:
            if screenShareInitiatedFrom == .endUser {
                showScreenSharePrompt(.startSession)
            }
        case .remoteControl:
            showScreenSharePrompt(.remoteControl)
        @unknown default:
            break
        }
    }
    .store(in: &cancellables)

L'enumerazione ScreenShareServiceState tiene traccia dell'avanzamento della sessione tramite .none, .pending, .authorizing, .active e .ended. L'editore delle richieste emette richieste di sessione e di controllo remoto che la tua app deve tradurre in richieste di consenso rivolte ai clienti.

Condivisione schermo dell'intero dispositivo (avanzata)

Per consentire agli agenti di vedere le schermate delle applicazioni esterne alla tua, devi implementare un'estensione di caricamento della trasmissione Apple. In questo modo iOS può acquisire lo schermo a livello di sistema e trasferire i frame video in modo sicuro all'SDK del progettoGoogle Cloud .

Aggiungere un target dell'estensione di trasmissione

  • Apri il progetto Xcode.

  • Vai a File > Nuovo > Destinazione…

  • Seleziona Estensione di caricamento della trasmissione.

  • Inserisci un nome per il target.

  • Deseleziona Includi estensione UI.

  • Crea il target, annotando il relativo ID pacchetto.

  • Modifica l'SDK di destinazione dell'estensione di trasmissione in iOS 12.0 o versioni successive.

Configurare la condivisione del portachiavi

La tua app e l'estensione dell'app devono condividere i segreti utilizzando il portachiavi iOS utilizzando il proprio gruppo di portachiavi.

  • In Xcode, vai alla scheda Signing & Capabilities (Firma e funzionalità) per entrambi i target.

  • Aggiungi la funzionalità Condivisione portachiavi.

  • Aggiungi io.cobrowse a Keychain Groups per entrambi i target.

Aggiorna il file Info.plist dell'app principale

Aggiungi l'ID pacchetto dell'estensione al Info.plist della tua app (non al Info.plist dell'estensione):

<key>CBIOBroadcastExtension</key>
<string>your.app.extension.bundle.ID.here</string>

Aggiungere CobrowseSDK al target dell'estensione di trasmissione

In Xcode, con il target dell'estensione di trasmissione selezionato, seleziona il pulsante + in Framework e librerie e aggiungi CobrowseSDK al target dell'estensione.

Implementare il gestore delle estensioni

Xcode aggiunge un file SampleHandler.swift al target che hai creato in precedenza. Sostituisci i contenuti del file con i seguenti in modo che erediti dalla classe di estensione ReplayKit dell'SDK:

import CobrowseSDK

class SampleHandler: CobrowseIOReplayKitExtension {
    // The Google Cloud project module automatically handles ReplayKit lifecycle methods
}

Crea ed esegui

Ora puoi creare ed eseguire la tua app. La funzionalità di accesso completo al dispositivo è disponibile solo sui dispositivi fisici e non funziona in iOS Simulator.

Se hai configurato tutto correttamente, dopo aver selezionato l'icona circolare blu dovresti visualizzare una schermata per selezionare l'estensione di trasmissione.

Limitazioni di SwiftUI per la condivisione schermo su iOS

  • Racchiudi gli elementi interattivi chiave con UIKit: per le aree che devono essere cliccabili da remoto, racchiudili come controlli UIKit (ad esempio UIButton) utilizzando UIViewRepresentable o UIViewControllerRepresentable. In questo modo i clic da remoto funzionano in modo affidabile utilizzando il percorso UIKit di Cobrowse.

  • Inoltra i tocchi remoti con la gestione personalizzata dei tocchi: per le aree SwiftUI che non possono essere sostituite con UIKit, utilizza i callback personalizzati dei tocchi di Cobrowse per inoltrare gli eventi di tocco remoto alla logica SwiftUI. L'implementazione prevede l'utilizzo di un UIView o di un riconoscitore di gesti personalizzato per gestire i callback, quindi il bridging degli eventi in SwiftUI (utilizzando UIViewRepresentable più un Coordinator, notifiche o chiusure).

  • Fornisci indicazioni o fallback a livello di prodotto: indica chiaramente nelle schermate SwiftUI che gli agenti possono solo visualizzare; l'utente deve toccare localmente oppure limita automaticamente le autorizzazioni di controllo remoto su queste pagine mantenendo l'accesso di sola visualizzazione.

Esempio di implementazione del wrapper UIKit

struct RemoteControlButton: UIViewRepresentable {
    let title: String
    let action: () -> Void

    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.addTarget(
            context.coordinator,
            action: #selector(Coordinator.buttonTapped),
            for: .touchUpInside
        )
        return button
    }

    func updateUIView(_ uiView: UIButton, context: Context) {
        uiView.setTitle(title, for: .normal)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(action: action)
    }

    class Coordinator: NSObject {
        let action: () -> Void

        init(action: @escaping () -> Void) {
            self.action = action
        }

        @objc func buttonTapped() {
            action()
        }
    }
}

// Usage in SwiftUI
struct ContentView: View {
    var body: some View {
        VStack {
            Text("This screen supports remote control")
            RemoteControlButton(title: "Clickable Button") {
                print("Button tapped remotely or locally")
            }
        }
    }
}

Chiamate pianificate

Le chiamate pianificate consentono agli utenti finali di prenotare una chiamata vocale futura anziché attendere in coda una chiamata immediata. Nell'SDK headless per iOS, utilizza i metadati di coda e canale per decidere se mostrare un punto di accesso alla programmazione nella tua app.

Rilevamento delle funzionalità

Cosa fornisce l'SDK: un flag scheduleEnabled su VoiceCallChannel che indica se la coda selezionata supporta la prenotazione di chiamate future, indipendentemente dalla disponibilità di chiamate vocali istantanee (live).

Cosa crei: la logica per controllare questo flag prima di mostrare il punto di ingresso della pianificazione.

Esempio di implementazione

Controlla scheduleEnabled sul canale di chiamata vocale della coda. Non utilizzare voiceCall != nil come proxy: una coda può supportare le chiamate vocali istantanee senza supportare le chiamate pianificate e viceversa.

func setupScheduledCallEntryPoint(for menu: QueueMenu) {
    guard let voiceChannel = menu.channels?.voiceCall else {
        // No voice channel at all hide both instant and scheduled call buttons
        hideCallNowButton()
        hideScheduleCallButton()
        return
    }

    // Instant voice call always show if voiceCall channel exists
    showCallNowButton()

    // Scheduled call check scheduleEnabled explicitly
    if voiceChannel.scheduleEnabled == true {
        showScheduleCallButton()
    } else {
        hideScheduleCallButton()
    }
}

Riferimento al modello dei dati

// VoiceCallChannel.swift
public struct VoiceCallChannel: Decodable {
    // ...
    public let scheduleEnabled: Bool?  // true if scheduled calls are supported
    // ...
}

// ChannelType.swift
public enum ChannelType {
    case scheduleCall(deflectionType: DeflectionType?)
    // ...
}

Puoi anche verificare la presenza di .scheduleCall nell'elenco dei canali attivati restituito da QueueMenu.getEnabledChannels().

Recuperare le fasce orarie disponibili

Il servizio di chiamata headless può recuperare gli orari degli appuntamenti disponibili per la coda selezionata. La tua app è proprietaria della UI del calendario o del selettore dell'ora.

Cosa fornisce l'SDK: timestamp degli slot disponibili per la coda selezionata, con supporto facoltativo per la riprogrammazione di una chiamata pianificata esistente.

Cosa crei: il calendario, il selettore dell'ora, la presentazione del fuso orario, la gestione dello stato vuoto e il comportamento di riprova.

Esempio di implementazione

Chiama getScheduledTimeSlots con l'ID menu della coda. Quando riprogrammi, passa l'ID della chiamata programmata esistente in modo che il servizio possa restituire la disponibilità appropriata.

let slots = try await CCAI.shared.callService?.getScheduledTimeSlots(
    menuId: menu.id,
    callIdForRescheduling: nil
) ?? []

Quando riprogrammi una chiamata pianificata esistente, passa l'ID chiamata esistente in modo che il servizio possa restituire la disponibilità appropriata:

let slots = try await CCAI.shared.callService?.getScheduledTimeSlots(
    menuId: menu.id,
    callIdForRescheduling: existingCallId
) ?? []

Creare una chiamata pianificata

Dopo che l'utente ha selezionato una fascia oraria, crea la chiamata pianificata tramite callService.

Cosa fornisce l'SDK: un'API createScheduledCall che prenota lo slot selezionato e restituisce il record della chiamata pianificata.

Cosa crei: convalida del numero di telefono E.164, UI di conferma, persistenza locale dell'ID della chiamata programmata e recupero per errori di programmazione.

Esempio di implementazione

Passa lo slot selezionato, il numero di telefono, l'ID coda e RecordingPermission.notAsked in un ScheduledCallRequest.

let response = try await CCAI.shared.callService?.createScheduledCall(
    ScheduledCallRequest(
        menuId: menu.id,
        phoneNumber: userPhoneNumber,
        scheduleTime: selectedSlot,
        recordingPermission: .notAsked,
        callIdForRescheduling: nil,
        customData: nil,
        ticketId: nil
    )
)
saveScheduledCallId(response?.id)

Il numero di telefono deve essere in formato E.164, ad esempio +14155551234.

Ripianificare una chiamata programmata

Per riprogrammare la chiamata, trasmetti l'ID della chiamata pianificata esistente come callIdForRescheduling.

Cosa fornisce l'SDK: le stesse API per la creazione e gli slot orari, con callIdForRescheduling utilizzato per identificare la prenotazione esistente.

Cosa crei: l'interfaccia utente "change time", la ricerca locale dell'ID chiamata pianificata memorizzato e lo stato di conferma aggiornato dopo la creazione della nuova prenotazione.

Esempio di implementazione

Trasferisci l'ID chiamata esistente a createScheduledCall dopo che l'utente ha selezionato un nuovo slot.

let response = try await CCAI.shared.callService?.createScheduledCall(
    ScheduledCallRequest(
        menuId: menu.id,
        phoneNumber: userPhoneNumber,
        scheduleTime: newSelectedSlot,
        recordingPermission: .notAsked,
        callIdForRescheduling: existingCallId,
        customData: nil,
        ticketId: nil
    )
)
saveScheduledCallId(response?.id)

Annullare una chiamata pianificata

Quando l'utente annulla una chiamata imminente, chiama cancelScheduledCall e cancella l'ID chiamata memorizzato localmente.

Cosa fornisce l'SDK: un'API di annullamento per un ID chiamata pianificata.

Cosa crei: il prompt di conferma, l'interfaccia utente di annullamento riuscito o di errore e la pulizia di qualsiasi stato di chiamata imminente memorizzato localmente.

Esempio di implementazione

Chiama cancelScheduledCall(callId:) con l'ID memorizzato, poi rimuovi l'ID dallo spazio di archiviazione dell'app.

try await CCAI.shared.callService?.cancelScheduledCall(callId: existingCallId)
clearScheduledCallId()

Rendi persistente l'ID della chiamata pianificata nella tua app se vuoi offrire un'esperienza di stato, riprogrammazione o annullamento di una chiamata imminente dopo il riavvio dell'app.

Email

Cosa fornisce l'SDK: l'indirizzo email di destinazione configurato per quella coda specifica, insieme a eventuali istruzioni che l'amministratore vuole che l'utente veda (ad esempio, Includi il numero di conto bancario).

Cosa crei: la schermata di composizione delle email e il delegato per monitorarne il completamento. Puoi creare un modulo personalizzato o utilizzare il MFMailComposeViewController integrato di Apple.

Esempio di implementazione

Questo codice estrae l'indirizzo email di assistenza e le istruzioni dall'SDK. Si apre il compositore di posta integrato di iOS e viene utilizzato MFMailComposeViewControllerDelegate per rilevare il momento esatto in cui l'utente seleziona Invia o Annulla.

import UIKit
import MessageUI

class SupportViewController: UIViewController, MFMailComposeViewControllerDelegate {
    // 1. Trigger the email flow
    func openEmailComposer(for menu: QueueMenu) {
        guard let emailChannel = menu.channels?.email else { return }

        if MFMailComposeViewController.canSendMail() {
            let mail = MFMailComposeViewController()
            mail.mailComposeDelegate = self

            // Use the metadata provided by the SDK
            mail.setToRecipients([emailChannel.email].compactMap { $0 })
            mail.setMessageBody(emailChannel.instructionMessage ?? "", isHTML: false)
            present(mail, animated: true)
        } else {
            showError("Mail services are not configured on this device.")
        }
    }

    // 2. Track the outcome using Apple's delegate
    func mailComposeController(
        _ controller: MFMailComposeViewController,
        didFinishWith result: MFMailComposeResult,
        error: Error?
    ) {
        controller.dismiss(animated: true)

        switch result {
        case .sent:
            print("Email successfully sent!")
        case .cancelled:
            print("User cancelled the email.")
        case .saved:
            print("Email saved as draft.")
        case .failed:
            print("Email failed to send: \(error?.localizedDescription ?? "Unknown error")")
        @unknown default:
            break
        }
    }
}

Cosa fornisce l'SDK: un elenco di URL (ad esempio il Centro assistenza della tua azienda, le norme di rimborso o un'app partner) e i relativi titoli visualizzati, esattamente come configurati nel portale amministratore.

Cosa crei: la logica per eseguire questi link quando un utente li seleziona, aprendoli in un browser in-app integrato o inviandoli all'app Safari.

Esempio di implementazione

Questo helper accetta una stringa URL e la presenta nel browser in-app di Apple.

import SafariServices

func openHelpCenterLink(_ urlString: String) {
    guard let url = URL(string: urlString) else { return }
    let safariViewController = SFSafariViewController(url: url)
    present(safariViewController, animated: true)
}

Su iOS, ExternalDeflectionLink non contiene un url di primo livello: i link si trovano all'interno di un array links: [ExternalDeflection]? interno e ogni voce abilitata contribuisce con un pulsante al menu del canale. Itera l'array e salta le voci il cui flag enabled non è true.

func setupDeflectionLinks(for queueMenu: QueueMenu) {
    // Clear any previously-rendered buttons
    helpCenterStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }

    guard let links = queueMenu.channels?.externalDeflectionLink?.links,
          !links.isEmpty else {
        helpCenterStackView.isHidden = true
        return
    }

    helpCenterStackView.isHidden = false

    for link in links where link.enabled == true {
        let button = UIButton(type: .system)
        button.setTitle(link.displayName, for: .normal)
        button.addAction(UIAction { [weak self] _ in
            self?.openHelpCenterLink(link.url)
        }, for: .touchUpInside)
        helpCenterStackView.addArrangedSubview(button)
    }
}

ExternalDeflectionLink espone enabled: Bool? e links: [ExternalDeflection]?. Ogni ExternalDeflection ha il proprio url e displayName. Se viene utilizzato QueueMenu.getEnabledChannels(), l'SDK emette una voce .externalDeflection(url:displayName:) per ogni link abilitato, quindi l'iterazione links diretta e l'iterazione dell'output dei canali abilitati producono lo stesso insieme di pulsanti.