Esta página orienta o time técnico que vai embarcar a página hospedada do Webchat cVortex em um aplicativo React Native, usando react-native-webview, com funcionamento completo no Android e no iOS. A página hospedada também é chamada de pré-chat, nome que aparece na URL (/pre-chat/).
Versões de referência: react-native-webview 13.x, React Native 0.8x, Android 8+ e iOS 14.3+.
Os trechos de código são ilustrativos: mostram a forma do que o aplicativo precisa fazer, e não uma implementação pronta. Ajuste à arquitetura do seu aplicativo.
1. Visão geral
O atendimento é uma página web que roda dentro da WebView do seu aplicativo. Ela resolve sozinha a conversa, o histórico, os anexos e o tempo real.
O que a página não consegue fazer de dentro de uma WebView, ela pede ao aplicativo: notificar, salvar um arquivo, abrir um link e guardar a conversa. É só isso que o aplicativo implementa.
Seu app (React Native)
└─ <WebView> ──── pede ────▶ notificar, baixar, abrir link, guardar
página hospedada ◀─── resposta ───
Declarar as funções do contrato é obrigatório. A página procura essas funções no objeto global window.CVortex. São elas que tornam o atendimento viável dentro de uma WebView. Sem elas, o botão não faz nada, ninguém é avisado e a conversa se perde, tudo sem nenhuma mensagem de erro na tela. O que varia de um aplicativo para outro é o que você faz dentro de cada função, e não quais funções você declara.
2. Carregar a página
O cVortex entrega uma URL por ambiente (tenant), já em https. O aplicativo acrescenta os parâmetros do atendimento e inApp=true.
const params = new URLSearchParams({
nome: "Maria Souza",
telefone: "47999990000",
inApp: "true" // declara que quem carrega a página é um aplicativo
});
const url = `${PRECHAT_URL}?${params}`;
|
Regra |
Motivo |
|---|---|
|
Use sempre |
Uma origem insegura desliga a persistência da conversa e o gravador de áudio. |
|
Inclua |
É a única declaração que chega antes de qualquer script do aplicativo rodar. No Android, a injeção pode chegar tarde demais; o parâmetro na URL, não. |
|
Os nomes dos parâmetros são definidos pelo cVortex. |
Variam por ambiente. Solicite a lista; normalmente nome e telefone são obrigatórios. |
|
Parâmetro vazio não é o mesmo que parâmetro ausente. |
Não envie a chave se não tiver o valor. |
Para os parâmetros disponíveis, consulte Webchat cVortex: formulário de entrada e recursos disponíveis.
3. O objeto de configuração
3.1 O que a página espera encontrar
Um objeto window.CVortex com todas as funções do contrato. Cada função recebe um objeto de dados e devolve uma Promise.
type CVortex = {
// Declaração: um aplicativo carregou esta página.
inApp: true;
// O formato de cada `data` está na seção da responsabilidade (seção 4).
onMessageReceived: (data: MessageReceivedData) => Promise<string | void>;
onDownload: (data: DownloadData) => Promise<string | void>;
onOpenLink: (data: OpenLinkData) => Promise<string | void>;
onError: (data: ErrorData) => Promise<string | void>;
onStateChanged: (state: unknown) => Promise<string | void>;
// A única que responde um valor, e não um veredito.
getState: () => Promise<any | null>;
};
A Promise é o retorno das funções: resolver significa "feito"; rejeitar significa "falhou".
Opcionalmente, resolva com um texto curto que descreva o desfecho (qual caminho foi tomado, por que não deu certo). Esse texto só é lido em log, nunca aparece na tela, e é cortado em 200 caracteres.
3.2 Como instalar o objeto em React Native
Duas restrições da plataforma moldam a solução:
-
O objeto precisa existir antes do primeiro script da página. Use
injectedJavaScriptBeforeContentLoaded, e nãoinjectedJavaScript. -
A injeção é de mão única.
injectJavaScriptnão devolve valor, então a Promise não pode ser resolvida pelo retorno da injeção.
Por isso o padrão é: a função injetada abre uma Promise, guarda essa Promise e avisa o aplicativo; o aplicativo faz o trabalho e injeta um script que resolve aquela Promise.
página ──► postMessage({ name, data, id }) "faça isto"
app ──► injectJavaScript(settle(id, ok, note)) "feito / falhou"
Cada chamada carrega um identificador. Devolva o mesmo identificador, e só a Promise daquela chamada é resolvida.
Esqueleto do script injetado (o importante é a estrutura):
// Registro das Promises abertas, uma por chamada.
var pending = {};
var nextId = 0;
var delegate = function (name, data) {
return new Promise(function (resolve, reject) {
// O chat envia um callId na maioria das chamadas; quando não envia,
// gere um local. Ele é a chave desta Promise.
var id = (data && data.callId) || "local-" + nextId++;
pending[id] = { resolve: resolve, reject: reject };
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: "host-call", name: name, data: data, id: id })
);
});
};
// O aplicativo chama esta função, de fora, para encerrar a chamada.
window.__cvortexSettle = function (id, isOk, note) {
var entry = pending[id];
if (!entry) return;
delete pending[id];
if (isOk) entry.resolve(note);
else entry.reject(new Error(note));
};
// Preserve o que já existir em window.CVortex em vez de substituir.
var host = window.CVortex || {};
host.onDownload = function (data) { return delegate("onDownload", data); };
// ...uma linha por função do contrato
window.CVortex = host;
true;
Três cuidados que custam caro se forem esquecidos:
-
Termine o script injetado com
true;. Sem isso, o iOS emite um aviso a cada injeção. -
Monte os valores do script de resposta com
JSON.stringify. Nomes de arquivo e textos de mensagem vêm de pessoas reais e quebram a concatenação de strings. -
Confira se a função existe antes de chamá-la no script de resposta. A página pode ter sido recarregada entre a pergunta e a resposta, porque o sistema derruba o renderizador quando o aplicativo vai para segundo plano.
3.3 getState é a exceção
As demais funções respondem um veredito; getState responde um valor. Como postMessage só carrega afirmações, a página manda uma pergunta com um identificador e o app responde injetando um script que entrega o valor àquela pergunta — mesmo mecanismo, outro retorno.
Deixe um prazo próprio no app (3 s, por exemplo) resolvendo null: se a resposta nunca vier, a página abre do zero em vez de ficar pendurada.
4. As responsabilidades do aplicativo
Todas as funções são obrigatórias. A coluna da direita mostra o que acontece quando uma delas falta. Nenhum desses sintomas vem acompanhado de mensagem de erro.
|
Função |
O aplicativo precisa |
Sem ela |
|---|---|---|
|
|
Guardar e devolver a conversa |
O cliente volta a um chat vazio e abre um segundo caso para o mesmo assunto |
|
|
Notificar |
Nenhum aviso de resposta do agente com o aplicativo fora da tela |
|
|
Salvar o arquivo |
O botão de baixar não faz nada |
|
|
Abrir o link |
O toque no link não faz nada |
|
|
Registrar o código |
Falha impossível de diagnosticar num aparelho que você não tem em mãos |
4.1 Guardar a conversa: getState e onStateChanged
O que permite retomar uma conversa fica no armazenamento da página, e esse armazenamento não é durável dentro de uma WebView: o iOS o apaga sozinho depois de uma semana sem uso, e o Android o descarta quando os dados do aplicativo são limpos. Perder esse estado faz o cliente abrir um segundo caso para o mesmo assunto.
Por isso a página entrega uma cópia ao aplicativo e pede essa cópia de volta na abertura seguinte.
|
Chamada |
Direção |
Responsabilidade do aplicativo |
|---|---|---|
|
|
página → aplicativo |
Gravar |
|
|
aplicativo → página |
Devolver o último |
// `persist` representa a persistência do seu aplicativo: AsyncStorage, sistema de arquivos ou MMKV.
// O contrato não depende do mecanismo, só do comportamento: gravar o que veio e devolver o que foi gravado.
const onStateChanged = async (state: unknown) => persist.write(state); // → ok/falhou
const getState = async () => (await persist.read()) ?? null;
Regras que não podem ser violadas:
-
Trate o conteúdo como opaco. Guarde exatamente como veio e devolva exatamente como estava. Não leia, não altere e não recrie campos: o formato é interno e muda sem aviso.
-
Guarde onde o dado sobrevive e só o aplicativo alcança. O mecanismo é indiferente (arquivo, chave-valor ou banco local). O que ele precisa ter é durabilidade (sobreviver ao fim do processo e à reinicialização do aparelho, sem ser removido pelo sistema por falta de espaço) e privacidade (o valor identifica a conversa e o cliente). Não servem: o diretório de cache, a memória do aplicativo e o
localStorageda própria página, que é justamente o armazenamento que se perde e a razão de este contrato existir. -
Responda ao
getStateem menos de 2 segundos. Esse é o limite da página; passado esse tempo, ela segue sem o estado e a conversa recomeça. Ler um arquivo local cabe nesse prazo com folga. Não faça chamadas de rede aqui. -
Nada guardado é uma resposta válida: devolva
null, sem rejeitar. -
Arquivo corrompido equivale a arquivo inexistente: devolva
null.
4.2 Notificar: onMessageReceived
type MessageReceivedData = {
message?: string; // texto da mensagem, quando houver
hasAttachments?: boolean; // quando não há texto, foi um anexo
};
Decida no lado nativo se o aplicativo está em segundo plano. O sinal de visibilidade da página não é confiável dentro de uma WebView: ela costuma se reportar visível mesmo com o aplicativo fora da tela. Use AppState.
// Não notificar porque o aplicativo está na tela é atender a chamada, e não falhar.
if (isForeground) return { ok: true, note: "skipped, app on screen" };
const body = data.message || (data.hasAttachments ? "Enviou um anexo" : "");
|
Plataforma |
Requisito |
|---|---|
|
Android 8+ |
Criar o canal de notificação na inicialização. Sem o canal, o sistema descarta a notificação sem aviso, com o mesmo sintoma de permissão negada. |
|
Android 13+ |
Solicitar |
|
iOS |
Solicitar a permissão na primeira execução. |
|
Ambas |
Não há entrega com o aplicativo fechado: a notificação é local e exige a WebView ativa. |
4.3 Baixar anexo: onDownload
type DownloadData = {
url: string; // URL pronta para baixar, com prazo de validade
fileName: string; // nome escolhido por quem enviou
};
A URL é temporária: baixe na hora, sem guardar para depois.
O caminho que funciona nas duas plataformas:
-
Baixar para uma pasta do aplicativo, criando uma subpasta por download para evitar colisão de nomes quando o mesmo anexo for baixado duas vezes.
-
Android: gravar na pasta escolhida pelo usuário via Storage Access Framework. Peça uma vez, guarde a escolha e reutilize nos próximos downloads.
-
iOS (e Android quando não houver pasta escolhida): abrir a folha de compartilhamento, que é como o iOS permite salvar o arquivo onde o usuário quiser.
Dois cuidados:
-
fileNamevem de quem enviou o arquivo. Trate como texto não confiável e remova separadores de caminho antes de usá-lo como nome.
const name = fileName?.replace(/[/\\]/g, "-").trim() || fallbackName;
-
Não deixe arquivo pela metade. Se a gravação falhar no meio, apague o destino. Um arquivo com o nome certo e zero byte é pior que nenhum, porque quem o encontra depois não tem como saber que o download falhou.
4.4 Abrir link: onOpenLink
type OpenLinkData = { url: string };
Esta é a única função que responde antes de fazer o trabalho, porque o trabalho consiste em sair da tela: depois que o navegador abre, nada que o aplicativo injete alcança a página, e o veredito se perde. Responda no último instante em que o aplicativo ainda está visível:
const canOpen = await Linking.canOpenURL(url);
replyWithVerdict(call, { ok: canOpen }); // responde primeiro...
if (canOpen) Linking.openURL(url); // ...e só então sai da tela
Mantenha setSupportMultipleWindows={false} e trate onOpenWindow como uma função vazia. Sem isso, um link pode abrir uma segunda WebView fora do contrato.
4.5 Registrar falhas: onError
O usuário continua vendo a mensagem de erro em português na tela. O código é o que permite diagnosticar a falha num aparelho que você não tem em mãos: envie-o para o log ou para a ferramenta de monitoramento de erros do aplicativo.
type ErrorData = { code: string; detail?: string };
|
Código |
Significado |
Causa mais comum |
|---|---|---|
|
|
A sessão da página acabou |
URL antiga, link reaproveitado ou sessão expirada |
|
|
Falta configuração do ambiente |
Erro de configuração no cVortex. Abra um chamado. |
|
|
Faltam parâmetros obrigatórios na URL |
Revise a montagem da URL (seção 2) |
|
|
Fora do horário de atendimento |
Comportamento esperado; a página exibe a tela própria |
|
|
Não foi possível consultar o horário |
Rede do aparelho ou indisponibilidade |
|
|
Não foi possível criar o caso |
Rede ou backend |
|
|
Chat já aberto em outra aba |
Não deve acontecer no aplicativo. Se aparecer, o objeto de configuração não chegou à página (seção 9). |
O horário de atendimento e a mensagem fora do expediente são configurados no Canal de WebChat.
5. Responder as chamadas
Um único onMessage recebe todas as chamadas, despacha pelo nome e devolve o veredito.
const handleMessage = (event: WebViewMessageEvent) => {
const call = JSON.parse(event.nativeEvent.data);
if (call?.type !== "host-call") return;
runHostCall(call); // despacha por call.name e responde usando call.id
};
5.1 O veredito
O veredito é um valor de sucesso ou falha, mais uma nota opcional.
-
Use
ok: falsesó para o que realmente falhou. Não notificar porque o aplicativo está na tela é atender a chamada, e não falhar. -
A nota é só para log. Nunca aparece na tela do usuário. Use-a para registrar qual caminho foi tomado (por exemplo,
"saved to the chosen folder"ou"no app for this link"). -
Não responder é pior que responder
false. A Promise fica pendente indefinidamente.
5.2 Reentrega ao voltar para a tela
injectJavaScript não executa com o aplicativo em segundo plano, que é justamente quando muitas chamadas acontecem: a notificação chega com o aplicativo fora da tela.
Guarde cada veredito num registro local, entregue na hora e entregue de novo quando o AppState voltar a active. Reentregar sem verificar é seguro: a primeira entrega aceita remove a pendência da página, e a segunda não encontra nada para resolver.
6. Configurar a WebView
<WebView
source={{ uri: url }}
injectedJavaScriptBeforeContentLoaded={HOST_SCRIPT}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
thirdPartyCookiesEnabled
mediaPlaybackRequiresUserAction={false}
allowsInlineMediaPlayback
allowsFullscreenVideo
mediaCapturePermissionGrantType="grant"
setSupportMultipleWindows={false}
onOpenWindow={() => undefined}
onRenderProcessGone={reload}
onContentProcessDidTerminate={reload}
onError={logWebViewError}
onHttpError={logWebViewError}
/>
|
Propriedade |
Se faltar |
|---|---|
|
|
A conversa não persiste nem dentro da mesma sessão |
|
|
Áudio recebido e som de aviso não tocam |
|
|
Vídeo abre em tela cheia sozinho no iOS |
|
|
O botão de tela cheia do vídeo fica inativo |
|
|
Gravador de áudio não funciona no iOS 15+ |
|
|
Links abrem fora do contrato |
|
|
O aplicativo fecha (Android) ou a WebView fica em branco (iOS) |
|
|
Falha de rede e erro HTTP deixam a tela em branco sem nenhum sinal |
Não use em produção: mixedContentMode="always", cacheEnabled={false} e usesCleartextTraffic. São recursos de desenvolvimento local; em produção, tudo é https.
7. Permissões e manifesto
Android
INTERNET
RECORD_AUDIO ← solicitada também em tempo de execução
MODIFY_AUDIO_SETTINGS
POST_NOTIFICATIONS ← solicitada em tempo de execução a partir do Android 13
-
RECORD_AUDIOprecisa ser solicitada em tempo de execução, antes de o usuário tocar no gravador. -
MODIFY_AUDIO_SETTINGSé concedida na instalação e é a mais esquecida: sem ela, o microfone falha comNotReadableErrore o botão fica cinza sem nenhuma mensagem. -
O seletor de arquivos e o repasse da permissão de mídia ao conteúdo web são tratados pelo
react-native-webviewa partir das permissões que o aplicativo já tem. Confirme na versão que você usa; numa integração nativa sem a biblioteca, isso precisa ser implementado (onShowFileChoosereonPermissionRequest).
Atenção: a origem que chega no pedido de permissão de mídia é a do conteúdo embarcado do cVortex, e não a da página carregada. Validar essa origem contra a URL da página hospedada faz o aplicativo negar a própria requisição.
iOS (Info.plist)
NSMicrophoneUsageDescription ← se a gravação de áudio estiver habilitada
NSCameraUsageDescription ← anexar foto pela câmera
NSPhotoLibraryUsageDescription ← anexar imagem da galeria
-
Use o armazenamento persistente da WebView. O modo não persistente apaga tudo ao fechar a tela.
-
O armazenamento da página pode ser apagado pelo sistema de qualquer forma, e é por isso que a seção 4.1 existe.
Rede
Libere por prefixo, e nunca por nome de arquivo:
-
os domínios cVortex da página e do conteúdo embarcado, inclusive WebSocket seguro (
wss://); -
fonts.googleapis.come as CDNs informadas pelo cVortex para o ambiente.
Proxy corporativo com inspeção de TLS é a causa mais comum de "abre e fica em branco". Se o aplicativo intercepta requisições, deixe as sub-requisições da página passarem intactas: um tipo MIME trocado ou uma página de erro HTML no lugar de um script derrubam a tela.
8. Homologação
Uma linha por responsabilidade. Teste no aparelho, nas duas plataformas.
|
# |
Teste |
Resultado esperado |
|---|---|---|
|
1 |
Abrir o aplicativo e iniciar o atendimento |
A conversa carrega, sem tela branca |
|
2 |
Enviar e receber mensagem |
Entrega imediata nos dois sentidos |
|
3 |
Fechar o aplicativo pela lista de recentes e reabrir |
A mesma conversa volta (seção 4.1) |
|
4 |
Deixar o aplicativo em segundo plano e pedir uma resposta ao agente |
A notificação nativa aparece |
|
5 |
Tocar na notificação |
O aplicativo abre na conversa |
|
6 |
Receber um anexo e baixar |
O arquivo é salvo e abre fora do aplicativo |
|
7 |
Baixar o mesmo anexo de novo |
Dois arquivos, sem sobrescrever |
|
8 |
Tocar num link enviado pelo agente |
O link abre no navegador do sistema |
|
9 |
Anexar um arquivo e uma foto |
O seletor abre nas duas plataformas |
|
10 |
Gravar um áudio |
A permissão é solicitada uma vez e a gravação funciona |
|
11 |
Girar a tela durante a conversa |
A conversa continua |
|
12 |
Ativar o modo avião durante a conversa |
A conexão é restabelecida ao desativar |
9. Sintomas e causas
Dentro de uma WebView, quase nada gera erro visível. Use esta tabela como atalho:
|
Sintoma |
Causa provável |
|---|---|
|
Tela branca ao abrir |
Rede bloqueando um recurso da página. Veja |
|
Botão de gravar cinza, sem mensagem |
Falta |
|
Botão de baixar não faz nada |
|
|
Link não abre |
|
|
Nenhuma notificação |
Canal não criado (Android 8+), permissão negada, ou o aplicativo estava na tela |
|
A conversa recomeça do zero a cada abertura |
|
|
Tela de chat "já aberto em outra aba" |
O objeto de configuração não chegou à página: a injeção falhou, ocorreu depois do primeiro script, ou ficou sem as funções do contrato (seção 3.1) |
|
Anexo salvo com 0 byte |
Gravação interrompida sem limpar o destino (seção 4.3) |
|
Tudo funciona em primeiro plano e nada em segundo plano |
Falta a reentrega de vereditos (seção 5.2) |
10. Resumo
O aplicativo entrega:
-
A URL montada com os parâmetros do ambiente e
inApp=true. -
O objeto
window.CVortexinstalado antes do conteúdo da página. -
Todas as funções do contrato declaradas: guardar a conversa, notificar, baixar, abrir link e registrar falhas.
-
Um veredito para cada chamada, reentregue ao voltar do segundo plano.
-
Permissões, manifesto e liberação de rede.
O cVortex entrega:
-
A URL do ambiente e a lista de parâmetros.
-
Toda a conversa: histórico, anexos, tempo real e horário de atendimento.
-
As chamadas do contrato, com identificador para correlacionar.
-
Os códigos de erro da seção 4.5.
Em caso de dúvida sobre um comportamento que não está descrito aqui, trate-o como não suportado e consulte o cVortex. Não dependa de detalhes internos da página: eles mudam sem aviso, e o contrato não.