Tu app de iOS presenta el widget nativo de DobroDesk con estado duradero por cuenta y preferencias de movimiento del sistema.
Añade el Swift Package publicado
En Xcode añade el repositorio https://github.com/comstud-io/dobrodesk-mobile-sdk.git en la versión 0.2.0 y añade DobroDeskSDK y DobroDeskUI al target de la app. El paquete binario admite iOS 15.1 y posteriores. La versión contiene artefactos. No incluye el código fuente de la implementación nativa.
https://github.com/comstud-io/dobrodesk-mobile-sdk.gitInicializa y presenta el cliente nativo
La app debe proporcionar un JWT de identidad del cliente de corta duración mediante credentialProvider. La autenticación nativa requiere un usuario firmado: usa un backend del host y nunca incluyas un signing secret ni un support API token en la app.
Con la concurrencia estricta de Swift 6, declara el loader del host como un closure @Sendable () async throws -> String y pásalo a credentialProvider.
import Foundation
import UIKit
import DobroDeskSDK
import DobroDeskUI
let loadDobroDeskToken: @Sendable () async throws -> String = {
// Replace this placeholder with a request to your host backend.
"short-lived-token-from-host-backend"
}
let client = try DobroDeskClient(
configuration: DobroDeskConfiguration(
apiOrigin: URL(string: "https://dobrodesk.com")!,
integrationId: "wgt_0123456789abcdef0123456789abcdef",
accountScope: currentCustomer.id,
locale: "en",
credentialProvider: loadDobroDeskToken,
pollInterval: 1
)
)
try await DobroDeskWidgetBridgePresenter.shared.present(
client: client,
options: ["locale": "en"],
from: viewController
)Usar un disparador propio o una superficie integrada
Para una superficie integrada, usa un UIView propio con constraints para safeArea top, leading y trailing y un anclaje inferior a keyboardLayoutGuide. Define una altura inicial de 560 puntos con priority .defaultHigh y monta solo después de que la vista esté conectada a window. Crea un presenter propio en cada montaje para que un close fallido o antiguo no cierre otra superficie.
El botón propio debe ser accesible con teclado y exponer aria-controls y aria-expanded. El wrapper del host debe capturar los errores de embed con do/catch y reenviarlos a su propio callback onError tipado para mostrar la recuperación. Al volver a montar, reutiliza el mismo cliente, la misma sesión y el mismo borrador.
Las API inline forman parte de los artefactos publicados de Mobile SDK 0.2.0. Usa la superficie inline cuando el host controla la ubicación y la presentación modal cuando el host necesita gestionar toda la pantalla.
import UIKit
import DobroDeskSDK
import DobroDeskUI
@MainActor
final class SupportPanelHost {
private let client: DobroDeskClient
private weak var parent: UIViewController?
private let onError: (Error) -> Void
private let supportContainer = UIView()
private var presenter: DobroDeskWidgetBridgePresenter?
private var mountTask: Task<Void, Never>?
private var mountGeneration = 0
private var isMounted = false
init(
client: DobroDeskClient,
parent: UIViewController,
onError: @escaping (Error) -> Void
) {
self.client = client
self.parent = parent
self.onError = onError
supportContainer.translatesAutoresizingMaskIntoConstraints = false
parent.view.addSubview(supportContainer)
let safeArea = parent.view.safeAreaLayoutGuide
let keyboardGuide = parent.view.keyboardLayoutGuide
let height = supportContainer.heightAnchor.constraint(equalToConstant: 560)
height.priority = .defaultHigh
NSLayoutConstraint.activate([
supportContainer.topAnchor.constraint(equalTo: safeArea.topAnchor),
supportContainer.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
supportContainer.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
supportContainer.bottomAnchor.constraint(equalTo: keyboardGuide.topAnchor),
height,
])
}
func mountAfterAppearance() {
guard mountTask == nil, !isMounted, let parent, supportContainer.window != nil else { return }
mountGeneration += 1
let generation = mountGeneration
let mountPresenter = DobroDeskWidgetBridgePresenter()
let client = self.client
let supportContainer = self.supportContainer
presenter = mountPresenter
mountTask = Task { @MainActor [weak self] in
defer {
if let self, self.mountGeneration == generation {
self.mountTask = nil
}
}
do {
try await mountPresenter.embed(
client: client,
options: ["locale": "en", "view": "home"],
in: supportContainer,
parent: parent
)
guard let self, self.mountGeneration == generation else {
mountPresenter.close()
return
}
isMounted = true
} catch {
guard let self, self.mountGeneration == generation else { return }
mountPresenter.close()
presenter = nil
onError(error)
}
}
}
func setVisible(_ visible: Bool) {
guard isMounted else { return }
presenter?.setEmbeddedVisible(visible)
}
func close() {
mountGeneration += 1
mountTask?.cancel()
mountTask = nil
presenter?.close()
presenter = nil
isMounted = false
}
}Mantén explícitos la presentación y el ciclo de la cuenta
Presenta desde un UIViewController en primer plano y llama a close cuando se elimine la pantalla host. Llama a client.resetSession() cuando el cliente descarte intencionadamente la conversación. El cliente iOS directo no tiene logout: restablécelo, descártalo y crea un cliente nuevo antes de cambiar accountScope.
El presenter nativo usa el mismo contrato visual del widget que el widget web. Omite reduceMotion salvo que el cliente haya elegido una opción explícita en la app. La preferencia del sistema para reducir movimiento sigue teniendo prioridad.
Gestionar carga, visibilidad y accesibilidad
Usa los hooks de carga o los eventos tipados para el estado de la pantalla anfitriona. La superficie muestra por sí misma la carga de mensajes, la espera y los reintentos para evitar un segundo spinner sobre el campo de entrada.
Oculta o muestra la superficie cuando una ruta cambia temporalmente y conserva el cliente. El widget respeta las preferencias del sistema para reducir movimiento. Quitar o cerrar la vista no descarta la sesión ni el borrador.
- Al volver a montar, reutiliza el mismo cliente y dimensiones estables del contenedor.
- Usa la función de visibilidad para ocultar temporalmente, no una función de reinicio de sesión.
- Finaliza la sesión solo al cerrar sesión o cambiar de cuenta de forma intencionada.
Verifica el flujo de iOS
- Abre el widget desde una pantalla en primer plano y confirma que el primer intento no crea un segundo controlador al tocar dos veces el botón.
- Inicia una conversación, cierra y vuelve a abrir el widget, y confirma que el borrador y el envío pendiente siguen vinculados a la misma cuenta.
- Gira o envía la app al segundo plano durante una operación pendiente y espera el estado explícito de reconexión o reintento antes de probar de nuevo.
- Restablece y descarta el cliente iOS directo antes de cambiar de cuenta. Después crea el cliente siguiente y confirma que no puede ver la conversación anterior.
