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_NAMESó 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 URLFlutter (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 comforceLocale=1para que ele prevaleça sobre a configuração do dispositivo. Nunca fixe um valor no código: umzh-Hansfixo é 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
ioscobre 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_appstoreoumacosconforme 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 oemailé rejeitada. secretKeyé a chave secreta da sua caixa de entrada (uma por caixa de entrada, disponível nas configurações dela). Semhmac, 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 dowebview_fluttersuportam<input type="file">para anexos. - iOS: HTTPS funciona sem exceção de ATS. O envio de imagens é um recurso embutido no widget: a
WKWebViewabre sozinha o seletor de fotos nativo, então você não precisa construir nada. Escolher da biblioteca de fotos não exige permissão. AdicioneNSCameraUsageDescriptionaoInfo.plistapenas 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.