Integração da página hospedada do Webchat em aplicativos móveis (React Native)

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 https. Nunca file:// nem HTML embutido.

Uma origem insegura desliga a persistência da conversa e o gravador de áudio.

Inclua inApp=true na query string, sempre.

É 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:

  1. O objeto precisa existir antes do primeiro script da página. Use injectedJavaScriptBeforeContentLoaded, e não injectedJavaScript.

  2. A injeção é de mão única. injectJavaScript nã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):

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

getState + onStateChanged

Guardar e devolver a conversa

O cliente volta a um chat vazio e abre um segundo caso para o mesmo assunto

onMessageReceived

Notificar

Nenhum aviso de resposta do agente com o aplicativo fora da tela

onDownload

Salvar o arquivo

O botão de baixar não faz nada

onOpenLink

Abrir o link

O toque no link não faz nada

onError

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

onStateChanged(state)

página → aplicativo

Gravar state e responder se conseguiu gravar

getState()

aplicativo → página

Devolver o último state gravado, ou null

// `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 localStorage da própria página, que é justamente o armazenamento que se perde e a razão de este contrato existir.

  • Responda ao getState em 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 POST_NOTIFICATIONS em tempo de execução.

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:

  1. 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.

  2. 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.

  3. 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:

  • fileName vem 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.

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

SESSION_INVALID

A sessão da página acabou

URL antiga, link reaproveitado ou sessão expirada

CONFIG_MISSING

Falta configuração do ambiente

Erro de configuração no cVortex. Abra um chamado.

INVALID_URL_PARAMS

Faltam parâmetros obrigatórios na URL

Revise a montagem da URL (seção 2)

OUTSIDE_WORKING_HOURS

Fora do horário de atendimento

Comportamento esperado; a página exibe a tela própria

WORKING_HOURS_UNAVAILABLE

Não foi possível consultar o horário

Rede do aparelho ou indisponibilidade

CASE_CREATE_FAILED

Não foi possível criar o caso

Rede ou backend

ALREADY_CONNECTED

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: false só 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

React TSX
<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

domStorageEnabled

A conversa não persiste nem dentro da mesma sessão

mediaPlaybackRequiresUserAction={false}

Áudio recebido e som de aviso não tocam

allowsInlineMediaPlayback

Vídeo abre em tela cheia sozinho no iOS

allowsFullscreenVideo

O botão de tela cheia do vídeo fica inativo

mediaCapturePermissionGrantType="grant"

Gravador de áudio não funciona no iOS 15+

setSupportMultipleWindows={false}

Links abrem fora do contrato

onRenderProcessGone / onContentProcessDidTerminate

O aplicativo fecha (Android) ou a WebView fica em branco (iOS)

onError / onHttpError

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_AUDIO precisa 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 com NotReadableError e 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-webview a 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 (onShowFileChooser e onPermissionRequest).

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.com e 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 onError / onHttpError e a liberação de rede (seção 7).

Botão de gravar cinza, sem mensagem

Falta MODIFY_AUDIO_SETTINGS, RECORD_AUDIO não foi solicitada em tempo de execução, ou falta mediaCapturePermissionGrantType (iOS)

Botão de baixar não faz nada

onDownload não instalada, ou o script não foi injetado antes do conteúdo

Link não abre

onOpenLink não instalada, ou setSupportMultipleWindows não está como false

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

getState não respondeu em 2 segundos, o estado foi gravado em pasta volátil, ou foi alterado em vez de devolvido como veio

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:

  1. A URL montada com os parâmetros do ambiente e inApp=true.

  2. O objeto window.CVortex instalado antes do conteúdo da página.

  3. Todas as funções do contrato declaradas: guardar a conversa, notificar, baixar, abrir link e registrar falhas.

  4. Um veredito para cada chamada, reentregue ao voltar do segundo plano.

  5. Permissões, manifesto e liberação de rede.

O cVortex entrega:

  1. A URL do ambiente e a lista de parâmetros.

  2. Toda a conversa: histórico, anexos, tempo real e horário de atendimento.

  3. As chamadas do contrato, com identificador para correlacionar.

  4. 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.

Veja também