Guías

Chat dentro de la app (WebView)

Integra el widget de chat completo en cualquier app móvil o de escritorio con una WebView: todas las funciones y actualizaciones automáticas.

La forma más rápida de añadir atención al cliente dentro de una app nativa es cargar en una WebView nuestra página alojada del widget a pantalla completa. Obtienes exactamente el mismo widget que en la web (Inicio, Mensajes, centro de ayuda, Buscar, emojis, adjuntos, marcas de tiempo, confirmaciones de lectura, cierre de la conversación), y se actualiza automáticamente. Una vez integrada, la app nunca necesita otra versión por cambios en el widget.

Inicio rápido

Apunta una WebView a esta URL. Si el usuario ha iniciado sesión, debes incluir externalId y email (e idealmente name); si no, el agente solo verá un visitante anónimo. Codifica cada valor para URL:

https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID&externalId=USER_ID&email=USER_EMAIL&name=USER_NAME

Solo cuando el usuario no ha iniciado sesión pasas a la forma anónima:

https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID

⚠️ ¿El agente ve un «visitante anónimo» sin correo? Es el error de integración número 1: a la URL de tu WebView le falta email/externalId. El servidor no puede inventárselos; la app tiene que enviarlos. Si el ID del visitante tiene la forma anon-…, no se envió nada.

JavaScript / cualquier WebView (sin Flutter)

En Electron, shells de escritorio, React Native o una WKWebView nativa en la que tú mismo defines la URL, construye la URL en JS. URLSearchParams codifica cada valor por ti:

// Build the support URL for the signed-in user before opening the WebView.
function buildSupportUrl(domain, user) {
  const params = new URLSearchParams({
    appId: 'YOUR_APP_ID',
    platform: 'windows',            // Strongly recommended: decides which help articles this visitor sees
                                    // ios / macos_appstore / macos / android / windows / web
    // locale: userSelectedLanguage, // Optional: only if your app has its own language switch; also add forceLocale: '1'
                                    // Omitted = follow the device language (covers the App Store's 50 localizations)
    // — Always pass these for signed-in users, or agents only see an "anonymous visitor" —
    externalId: user.id,            // The user's unique ID in your system
    email: user.email,              // The user's email (important)
    name: user.name || '',          // The user's name (optional)
    // hmac: user.hmac,             // Optional: computed on your server to prevent impersonation (see HMAC below)
    // attrs: JSON.stringify({ plan: user.plan, expiresAt: user.expireAt }), // Optional: custom attributes
  });
  return `https://${domain}/api/widget-embed?${params.toString()}`;
}

// Usage: load the returned url in your WebView (instead of the old ?appId=...-only URL).
const url = buildSupportUrl('askais.com', currentUser);
myWebView.loadURL(url); // Electron: win.loadURL(url); native: load this URL

Flutter (webview_flutter)

Añade webview_flutter: ^4.x a pubspec.yaml y luego:

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class SupportPage extends StatefulWidget {
  const SupportPage({super.key});
  @override
  State<SupportPage> createState() => _SupportPageState();
}

class _SupportPageState extends State<SupportPage> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    // For signed-in users always include externalId + email, or agents see an "anonymous visitor" (no name / email).
    // Pass only appId + locale when the user is not signed in. Uri.https URL-encodes the values.
    final uri = Uri.https('askais.com', '/api/widget-embed', {
      'appId': 'YOUR_APP_ID',
      'platform': Platform.isIOS ? 'ios' : 'android', // Strongly recommended: decides which help articles are visible
      // No locale = follow the device language (covers the App Store's 50 localizations).
      // Only pass it if your app has its own language switch, together with 'forceLocale': '1':
      // 'locale': appSettings.selectedLanguage,
      'externalId': user.id, // The user's unique ID in your system (important)
      'email': user.email, // The user's email (important)
      'name': user.name, // Optional
      // 'hmac': hmacFromYourServer, // Optional: computed on your server to prevent impersonation
    });
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.white)
      ..loadRequest(uri);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Support')),
      // Full-screen: drop the appBar and use your own close button.
      body: SafeArea(child: WebViewWidget(controller: _controller)),
    );
  }
}

// Open it from wherever your "Support" button lives:
// Navigator.push(context,
//   MaterialPageRoute(builder: (_) => const SupportPage()));

Para WebViews de iOS/Android en otros stacks (Swift WKWebView, Kotlin WebView, React Native react-native-webview), la idea es la misma: cargar la URL y activar JavaScript.

iOS y Android nativos

No hay SDK que instalar: construyes la misma URL y se la pasas a la WebView. Ojo con el caso del Mac: una compilación para App Store y una compilación DMG corren sobre el mismo sistema operativo, así que el valor tiene que salir de un flag de compilación, no de una comprobación en tiempo de ejecución.

// ── iOS (Swift / WKWebView) ────────────────────────────────
// On Mac Catalyst / macOS, set platform to macos_appstore or macos.
#if targetEnvironment(macCatalyst)
let platform = "macos_appstore"   // App Store build; a DMG build from your website passes "macos"
#else
let platform = "ios"              // iPhone and iPad are both ios
#endif

var comps = URLComponents(string: "https://\(domain)/api/widget-embed")!
comps.queryItems = [
    .init(name: "appId", value: "YOUR_APP_ID"),
    .init(name: "platform", value: platform),
    .init(name: "externalId", value: user.id),     // Required for signed-in users
    .init(name: "email", value: user.email),       // Required for signed-in users
    .init(name: "name", value: user.name),
    // Only pass these two if your app has its own language switch:
    // .init(name: "locale", value: settings.language),
    // .init(name: "forceLocale", value: "1"),
]
webView.load(URLRequest(url: comps.url!))          // URLComponents URL-encodes the values


// ── Android (Kotlin / WebView) ─────────────────────────────
val url = Uri.parse("https://$domain/api/widget-embed")
    .buildUpon()
    .appendQueryParameter("appId", "YOUR_APP_ID")
    .appendQueryParameter("platform", "android")
    .appendQueryParameter("externalId", user.id)
    .appendQueryParameter("email", user.email)
    .appendQueryParameter("name", user.name)
    // .appendQueryParameter("locale", settings.language)
    // .appendQueryParameter("forceLocale", "1")
    .build()

webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true          // The widget uses localStorage to remember the visitor
webView.loadUrl(url.toString())

Parámetros de la URL

  • appId (obligatorio): el App ID de tu bandeja de entrada.
  • locale: idioma de la interfaz. Omítelo en casi todos los casos: así el widget sigue el idioma del dispositivo que informa la WebView, que es lo que esperan los usuarios. La interfaz está traducida a las 50 localizaciones de App Store Connect; cualquier idioma fuera de esa lista usa el inglés. Pásalo solo cuando tu app tenga su propio selector de idioma: en ese caso, envía el idioma que eligió el usuario, junto con forceLocale=1 para que se imponga a la configuración del dispositivo. Nunca pongas un valor fijo en el código: un zh-Hans fijo es la razón por la que una app en japonés puede acabar mostrando un widget en chino.
  • platform (muy recomendado): la compilación que usa el visitante. Decide qué artículos de ayuda ve y si la IA responde según las normas de la App Store. Si lo omites, la plataforma se deduce del user agent de la WebView: así se reconocen iPhone, Android y Windows, pero no se distingue una compilación de Mac App Store de una DMG, ni un iPad en modo escritorio. Valores: ios (iPhone y iPad), macos_appstore (compilación de Mac App Store), macos (compilación DMG de descarga directa), android, windows, web.
  • externalId: el ID único del usuario en tu app. Pasarlo permite al agente reconocer al usuario y unificar su historial.
  • email, name, avatarUrl: perfil que ven los agentes.
  • hmac: firma de identidad (ver más abajo). Opcional.

Ocultar contenido por plataforma (cumplimiento de la App Store)

La norma 3.1.1 de la App Store no permite que una app de iOS muestre contenido sobre compras externas, suscripciones o referidos. Por eso cada artículo de ayuda y cada pregunta frecuente tiene un ajuste de visibilidad por plataforma: abre la página Centro de ayuda o Preguntas frecuentes en tu panel y activa los chips de plataforma de ese elemento. Los visitantes de una plataforma seleccionada dejan de verlo en la lista y no pueden abrirlo con un enlace directo.

La IA no responde a partir de los artículos de ayuda ni de las preguntas frecuentes; responde con lo que añadiste en «Training». Los archivos y pares de preguntas y respuestas de ahí tienen los mismos chips de plataforma, y un elemento oculto para una plataforma no se usa en las respuestas a los visitantes de esa plataforma. Ocúltalo en ambos sitios si la IA tampoco debe mencionarlo.

Dos cosas deciden si esto funciona de forma fiable. Primero, tu app debe enviar platform; deducirlo del user agent es solo un recurso de reserva. Segundo, el valor tiene que reflejar la compilación, no solo el sistema operativo:

  • El iPad no va aparte. Ejecuta la misma app de iOS con las mismas normas, así que ios cubre iPhone y iPad a la vez.
  • El Mac tiene dos compilaciones. Una compilación de Mac App Store está sujeta a la norma 3.1.1; un DMG que distribuyes desde tu propia web, no. Son el mismo sistema operativo, así que nada en tiempo de ejecución puede distinguirlas: decídelo al compilar (con un flag de Xcode, por ejemplo) y envía macos_appstore o macos según corresponda. Si te equivocas, o la compilación de la tienda muestra contenido de suscripciones durante la revisión, o la compilación DMG oculta la mitad de tu centro de ayuda sin motivo.

Los cambios se aplican al instante: el ajuste está en tu panel, no en la app, así que nunca tienes que publicar una versión para cambiar lo que se ve.

Identidad verificada (HMAC, opcional)

Para demostrar que un externalId es de verdad tu usuario con sesión iniciada (y evitar suplantaciones), pasa una firma HMAC. Calcúlala en tu servidor y nunca pongas la clave secreta en la app.

  • hmac = HMAC-SHA256(secretKey, externalId), en hexadecimal y minúsculas.
  • El payload es el externalId, así que envíalo siempre: sin él, el widget usa su propio ID anónimo y una firma sobre el email se rechaza.
  • secretKey es la clave secreta de tu bandeja de entrada (una por bandeja; la encontrarás en su configuración). Sin hmac, el visitante se trata como no verificado.
import { createHmac } from 'node:crypto';

// SECRET_KEY lives only on your server, never ship it inside the app.
const hmac = createHmac('sha256', SECRET_KEY)
  .update(externalId)   // exactly the externalId you pass to the widget
  .digest('hex');

// Return { externalId, email, hmac } to the app, which appends them to the URL.

Notas por plataforma

  • Android: conserva el permiso INTERNET; intercepta el botón Atrás para que primero retroceda dentro de la WebView. Las versiones recientes de webview_flutter admiten <input type="file"> para adjuntos.
  • iOS: HTTPS funciona sin excepciones de ATS. La subida de imágenes es una función integrada del widget: WKWebView abre por sí sola el selector de fotos nativo, así que no tienes que construir nada. Elegir de la fototeca no requiere permiso. Añade NSCameraUsageDescription a Info.plist solo si quieres la opción de cámara «Hacer foto»; sin esa clave, la app se cierra cuando el usuario la toca.

¿Prefieres una interfaz nativa?

Si necesitas una pantalla de chat nativa hecha a mano en lugar de una WebView, usa los clientes de solo lógica de la página SDK para móviles. Para la mayoría de las apps, la WebView de arriba se publica antes y siempre tiene todas las funciones.