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. Consideranilofalsecome stato"don't offer an instant call button". A differenza divoiceCall != 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 diRecordingPermission.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. Quandodeflected == true, mostra il messaggiodeflectedReasonconfigurato 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)
}
}
Consenso alla registrazione
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:
Esamina
VoiceCallChanneldella coda selezionata.Verifica che le chiamate istantanee siano attive e non deviate.
Gestisci il consenso alla registrazione in base ai
recordingOptiondella coda.Richiedi l'autorizzazione microfono.
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: quandofalse, l'SDK non riproduce gli annunci di sistema; esegui il rendering autonomamente utilizzandoaudibleMessageService.callKitEnabled: Bool: quandofalse, 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.cobrowsea 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
Approcci consigliati per l'interazione remota in SwiftUI
Racchiudi gli elementi interattivi chiave con UIKit: per le aree che devono essere cliccabili da remoto, racchiudili come controlli UIKit (ad esempio
UIButton) utilizzandoUIViewRepresentableoUIViewControllerRepresentable. 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
UIViewo di un riconoscitore di gesti personalizzato per gestire i callback, quindi il bridging degli eventi in SwiftUI (utilizzandoUIViewRepresentablepiù unCoordinator, 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.
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
}
}
}
Link di deviazione esterni
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)
}
Esempio di integrazione completa (itera tutti i link abilitati)
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.