Ir al contenido

Pregunta a la doc

Las respuestas vienen solo de esta documentación, con fuentes. Para temas específicos de tu cuenta, la doc te dirige al responsable correcto.

¿Qué estás buscando?

Estás hablando con un asistente de IA. Puede equivocarse, así que verifica la información importante.

App integrada en Android

En Android, tu app de asesores carga la app integrada en una WebView en lugar de un iframe. La app se comunica con tu código Kotlin a través de un puente que inyectas, con los mismos mensajes que en la web, más lo que necesita un teléfono: el botón atrás del sistema, los enlaces que se abren fuera de la app y un escáner que tu app lanza cuando la WebView no puede escanear.

Lo demás no cambia: la configuración en el panel, las claves de firma y el token de asesor que firma tu backend.

Tu app Android (WebView) Dialog
loadUrl ────────────────────────────────▶ app integrada
◀── dialog:ready
──▶ dialog:auth { token } (firmado por tu backend)
──▶ dialog:shopper { email }
──▶ dialog:back (botón atrás del sistema)
◀── dialog:back-result { handled }
◀── dialog:scan-requested (la WebView no puede escanear)
──▶ dialog:open-product { productId }
◀── dialog:open-url { url } (el enlace de pago)
◀── dialog:pre-cart-sent

La app se carga en el nivel superior de la WebView, no dentro de un iframe: en Android no hay ningún origen autorizado que declarar en el panel. Es tu app la que limita el puente al origen de Dialog.

  • androidx.webkit 1.3 o posterior, para WebViewCompat.addWebMessageListener. Comprueba WebViewFeature.WEB_MESSAGE_LISTENER en tiempo de ejecución.
  • androidx.browser, para abrir el enlace de pago en un Custom Tab.
  • El permiso CAMERA en tu manifiesto, solicitado en tiempo de ejecución, si el asesor escanea en la app.
private const val DIALOG_ORIGIN = "https://assistant-app.askdialog.com"
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
// Sin el puente, la app no recibe su token: avisa en lugar de cargarla.
if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
showWebViewUpdateRequired()
return
}
// Antes de loadUrl: el puente se inyecta al cargar la página.
WebViewCompat.addWebMessageListener(webView, "DialogHost", setOf(DIALOG_ORIGIN)) {
_, message, _, _, replyProxy ->
dialogHost = replyProxy
message.data?.let { onDialogMessage(JSONObject(it)) }
}
webView.loadUrl("$DIALOG_ORIGIN/?apiKey=TU_CLAVE_API_PUBLICA&locale=es-ES&scan=native")

El puente debe llamarse DialogHost, y su regla de origen debe ser exactamente el origen de Dialog: así el puente solo existe en la página de Dialog, nunca en una página a la que lleve un enlace. Los mensajes son texto JSON en ambos sentidos.

Parámetro Obligatorio Descripción
apiKey sí Tu clave API pública, como en la web.
locale no Una locale BCP 47 (es-ES, en-GB). Por defecto, la del dispositivo.
countryCode no País en dos letras, cuando el mercado difiere de la región de la locale.
scan no native cuando tu app sabe escanear códigos por sí misma. Consulta Escanear.

Tu app solo puede responder después del primer mensaje de la app: el replyProxy llega con dialog:ready.

private var dialogHost: JavaScriptReplyProxy? = null
private fun send(type: String, vararg fields: Pair<String, Any>) {
val message = JSONObject().put("type", type)
fields.forEach { (key, value) -> message.put(key, value) }
dialogHost?.postMessage(message.toString())
}
private fun onDialogMessage(message: JSONObject) {
when (message.getString("type")) {
"dialog:ready", "dialog:token-expiring" -> lifecycleScope.launch {
send("dialog:auth", "token" to backend.fetchDialogToken())
}
"dialog:back-result" -> if (!message.getBoolean("handled")) finish()
"dialog:open-url" -> CustomTabsIntent.Builder().build()
.launchUrl(this, Uri.parse(message.getString("url")))
"dialog:scan-requested" -> scanner.scan { code ->
send("dialog:open-product", "productId" to code)
}
"dialog:pre-cart-sent" -> {
// draftOrderName, invoiceUrl, invoiceSent
}
}
}
Mensaje Contenido Cuándo
dialog:ready ninguno La app está cargada. Responde con dialog:auth y luego dialog:shopper si lo conoces.
dialog:token-expiring ninguno Unos 60 segundos antes de que caduque el token, o tras un rechazo de Dialog. Responde con un nuevo dialog:auth.
dialog:back-result { handled } La respuesta a dialog:back. false en la pantalla de inicio de la app: cierra tú la pantalla.
dialog:open-url { url } Un enlace que la app no puede abrir: una WebView lo cargaría en lugar de la app. Hoy, el enlace de pago de un pre-carrito enviado. Ábrelo en un Custom Tab.
dialog:scan-requested ninguno El asesor quiere escanear y la WebView no puede. Escanea y responde con dialog:open-product.
dialog:pre-cart-sent { draftOrderName, invoiceUrl, invoiceSent } Como en la web.
Mensaje Contenido Función
dialog:auth { token } El token de asesor, como en la web.
dialog:shopper { email?, customerId? } El cliente al que atiende el asesor, como en la web.
dialog:back ninguno El botón atrás del sistema. La app cierra su panel abierto o vuelve una pantalla, y responde con dialog:back-result.
dialog:open-product { productId } Abre el asistente en un producto: el código que leyó tu escáner, o un producto que eligió tu app.

Envía cada pulsación de atrás a la app, y sal de la pantalla solo cuando responda que no tenía adónde volver.

onBackPressedDispatcher.addCallback(this) {
if (dialogHost == null) finish() else send("dialog:back")
}

La app escanea códigos QR con la cámara cuando la WebView lo permite. No todas las WebView de Android lo permiten, según el dispositivo. Con scan=native en la URL, la app recurre a tu escáner cuando no puede escanear, o cuando el asesor rechazó la cámara: envía dialog:scan-requested y tú respondes con dialog:open-product. Sirve cualquier escáner, por ejemplo la lectura de códigos de barras de ML Kit.

El código QR contiene el identificador del producto, como en la web. El asesor siempre puede escribir el código en su lugar.

Para el escáner de la WebView, concede la cámara a la página de Dialog una vez que tu app tenga el permiso CAMERA:

webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
val fromDialog = request.origin.toString().trimEnd('/') == DIALOG_ORIGIN
if (fromDialog && PermissionRequest.RESOURCE_VIDEO_CAPTURE in request.resources) {
request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
} else {
request.deny()
}
}
}