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.
Cómo encaja todo
Sección titulada «Cómo encaja todo»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-sentLa 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.
Requisitos
Sección titulada «Requisitos»androidx.webkit1.3 o posterior, paraWebViewCompat.addWebMessageListener. CompruebaWebViewFeature.WEB_MESSAGE_LISTENERen tiempo de ejecución.androidx.browser, para abrir el enlace de pago en un Custom Tab.- El permiso
CAMERAen tu manifiesto, solicitado en tiempo de ejecución, si el asesor escanea en la app.
Cargar la app
Sección titulada «Cargar la app»private const val DIALOG_ORIGIN = "https://assistant-app.askdialog.com"
webView.settings.javaScriptEnabled = truewebView.settings.domStorageEnabled = truewebView.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. |
Mensajes
Sección titulada «Mensajes»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 } }}De la app a tu código
Sección titulada «De la app a tu código»| 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. |
De tu código a la app
Sección titulada «De tu código a la app»| 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. |
El botón atrás
Sección titulada «El botón atrás»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")}Escanear
Sección titulada «Escanear»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() } }}Siguientes pasos
Sección titulada «Siguientes pasos»- App integrada: la configuración en el panel y el token de asesor.
- Integración del catálogo: el catálogo que explora la app integrada.
