Guias

Chat dentro do app (WebView)

Incorpore o widget de chat completo em qualquer app mobile ou desktop com uma WebView: todos os recursos e atualização automática.

A forma mais rápida de colocar atendimento ao cliente dentro de um app nativo é carregar nossa página hospedada do widget em tela cheia em uma WebView. Você tem exatamente o mesmo widget do site (Início, Mensagens, central de ajuda, Pesquisar, emojis, anexos, horários, confirmação de leitura, encerramento da conversa), e ele se atualiza automaticamente. Depois de integrado, o app nunca mais precisa de uma nova versão por causa de mudanças no widget.

Início rápido

Aponte uma WebView para esta URL. Para um usuário logado, é obrigatório incluir externalId e email (e, de preferência, name); caso contrário, o agente só verá um visitante anônimo. Codifique cada valor para URL:

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

Só use a forma anônima quando o usuário não estiver logado:

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

⚠️ O agente vê um “visitante anônimo” sem e-mail? Esse é o erro de integração número 1: falta email/externalId na URL da sua WebView. O servidor não tem como inventar esses dados; o app precisa enviá-los. Se o ID do visitante tiver a forma anon-…, nada foi enviado.

JavaScript / qualquer WebView (sem Flutter)

No Electron, em shells desktop, no React Native ou em uma WKWebView nativa em que você mesmo define a URL, monte a URL em JS. O URLSearchParams codifica cada valor para você:

// 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)

Adicione webview_flutter: ^4.x ao pubspec.yaml e depois:

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 em outras stacks (Swift WKWebView, Kotlin WebView, React Native react-native-webview), a ideia é a mesma: carregar a URL e ativar o JavaScript.

iOS e Android nativos

Não há SDK para instalar: você monta a mesma URL e a entrega à WebView. Atenção ao caso do Mac: um build da App Store e um build DMG rodam no mesmo sistema operacional, então o valor precisa vir de uma flag de build, e não de uma verificação em tempo de execução.

// ── 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 URL

  • appId (obrigatório): o App ID da sua caixa de entrada.
  • locale: idioma da interface. Omita na grande maioria dos casos: assim o widget segue o idioma do dispositivo informado pela WebView, que é o que os usuários esperam. A interface está traduzida para todas as 50 localizações do App Store Connect; qualquer idioma fora dessa lista cai para o inglês. Envie apenas quando o seu app tiver um seletor de idioma próprio: nesse caso, mande o idioma escolhido pelo usuário, junto com forceLocale=1 para que ele prevaleça sobre a configuração do dispositivo. Nunca fixe um valor no código: um zh-Hans fixo é o motivo de um app em japonês acabar mostrando um widget em chinês.
  • platform (altamente recomendado): qual build o visitante está usando. Define quais artigos de ajuda ele vê e se a IA responde seguindo as regras da App Store. Sem ele, a plataforma é deduzida pelo user agent da WebView: isso reconhece iPhone, Android e Windows, mas não distingue um build da Mac App Store de um build DMG, nem um iPad no modo desktop. Valores: ios (iPhone e iPad), macos_appstore (build da Mac App Store), macos (build DMG distribuído diretamente), android, windows, web.
  • externalId: o ID único do usuário no seu app. Enviá-lo permite que o agente reconheça o usuário e junte o histórico dele.
  • email, name, avatarUrl: perfil exibido para os agentes.
  • hmac: assinatura de identidade (veja abaixo). Opcional.

Ocultar conteúdo por plataforma (conformidade com a App Store)

A regra 3.1.1 da App Store não permite que um app iOS mostre conteúdo sobre compras externas, assinaturas ou indicações. Por isso, cada artigo de ajuda e cada pergunta frequente tem uma configuração de visibilidade por plataforma: abra a página Central de ajuda ou Perguntas frequentes no seu painel e ative os chips de plataforma naquele item. Visitantes em uma plataforma selecionada deixam de vê-lo na lista e não conseguem abri-lo por link direto.

A IA não responde com base nos artigos de ajuda nem nas perguntas frequentes; ela responde com o que você adicionou em “Training”. Os arquivos e pares de perguntas e respostas de lá têm os mesmos chips de plataforma, e um item oculto para uma plataforma não é usado nas respostas aos visitantes dessa plataforma. Oculte-o nos dois lugares se a IA também não deve mencioná-lo.

Duas coisas decidem se isso funciona de forma confiável. Primeiro, seu app deve enviar platform; deduzir pelo user agent é só um recurso de reserva. Segundo, o valor precisa refletir o build, e não só o sistema operacional:

  • O iPad não é separado. Ele roda o mesmo app iOS sob as mesmas regras, então ios cobre iPhone e iPad juntos.
  • O Mac tem dois builds. Um build da Mac App Store está sujeito à regra 3.1.1; um DMG distribuído pelo seu próprio site, não. Como é o mesmo sistema operacional, nada em tempo de execução consegue diferenciá-los: decida isso na compilação (com uma flag do Xcode, por exemplo) e envie macos_appstore ou macos conforme o caso. Se errar, ou o build da loja expõe conteúdo de assinatura durante a revisão, ou o build DMG esconde metade da sua central de ajuda sem motivo.

As alterações valem na hora: a configuração fica no seu painel, não no app, então você nunca precisa lançar uma versão para mudar o que fica visível.

Identidade verificada (HMAC, opcional)

Para comprovar que um externalId é mesmo o seu usuário logado (e impedir que alguém se passe por ele), envie uma assinatura HMAC. Calcule-a no seu servidor; nunca coloque a chave secreta no app.

  • hmac = HMAC-SHA256(secretKey, externalId), com saída em hexadecimal minúsculo.
  • O payload é o externalId, então sempre o envie: sem ele, o widget usa o próprio ID anônimo, e uma assinatura sobre o email é rejeitada.
  • secretKey é a chave secreta da sua caixa de entrada (uma por caixa de entrada, disponível nas configurações dela). Sem hmac, o visitante é tratado como não 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.

Observações por plataforma

  • Android: mantenha a permissão INTERNET; intercepte o botão Voltar para que ele volte primeiro dentro da WebView. Versões recentes do webview_flutter suportam <input type="file"> para anexos.
  • iOS: HTTPS funciona sem exceção de ATS. O envio de imagens é um recurso embutido no widget: a WKWebView abre sozinha o seletor de fotos nativo, então você não precisa construir nada. Escolher da biblioteca de fotos não exige permissão. Adicione NSCameraUsageDescription ao Info.plist apenas se quiser a opção de câmera “Tirar Foto”; sem essa chave, o app fecha quando o usuário toca nela.

Prefere uma interface nativa?

Se você precisa de uma tela de chat nativa feita à mão em vez de uma WebView, use os clientes só de lógica da página SDKs para mobile. Para a maioria dos apps, a WebView acima é mais rápida de lançar e sempre tem todos os recursos.