VyntricaVyntricaDocumentaçãoLista de espera

Widget em app (WebView)

O widget da Vyntrica também funciona dentro de aplicativos Android e iOS. Hoje isso é feito por WebView: o app abre uma página com o chat.js e o atendimento roda igual ao do site, com a mesma chave de API e o mesmo histórico no painel.

Ainda não existe SDK nativo para Android, iOS, React Native ou Flutter. O caminho abaixo usa só componentes padrão de cada plataforma.

Como funciona

  1. Você publica uma página simples com o snippet do widget, no seu próprio domínio (com HTTPS).
  2. O app abre essa página numa WebView, em tela cheia ou numa aba de atendimento.
  3. A pessoa toca no botão do chat e conversa normalmente.

Use sempre uma página hospedada em HTTPS. Gravação de áudio pelo navegador só funciona em contexto seguro, e assim você atualiza a página sem publicar nova versão do app.

Página de atendimento

Exemplo de https://seusite.com/atendimento.html:

<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Atendimento</title>
  <style>html, body { margin: 0; height: 100%; background: #ffffff; }</style>
</head>
<body>
  <script
    src="https://api.vyntrica.com/widget-cdn/chat.js"
    data-api-key="SUA_CHAVE_DE_API"
    data-api-url="https://api.vyntrica.com"
    data-color="#8b5cf6"
    async
  ></script>
</body>
</html>

Os atributos são os mesmos da instalação no site.

O que a WebView precisa liberar

Recurso do widgetO que o app precisa
Conversa e históricoJavaScript e armazenamento local (DOM storage) ligados. O histórico fica no localStorage do aparelho.
Envio de arquivoTratar o seletor de arquivo da WebView. Sem isso, o botão de anexo não abre nada no Android.
Mensagem de áudioPermissão de microfone no app e liberação da captura de áudio na WebView.
Links (anexos, privacidade)Abrem em nova aba. O app decide se abre no navegador do aparelho ou na própria WebView.

React Native

Com o pacote react-native-webview:

import { Linking } from 'react-native'
import { WebView } from 'react-native-webview'

export default function Atendimento() {
  return (
    <WebView
      source={{ uri: 'https://seusite.com/atendimento.html' }}
      javaScriptEnabled
      domStorageEnabled
      mediaCapturePermissionGrantType="grant"
      onOpenWindow={(e) => Linking.openURL(e.nativeEvent.targetUrl)}
    />
  )
}

No Android, declare android.permission.RECORD_AUDIO no AndroidManifest.xml e peça a permissão em tempo de execução antes de abrir a tela. No iOS, adicione NSMicrophoneUsageDescription no Info.plist.

Flutter

Com os pacotes webview_flutter, url_launcher e file_picker:

import 'package:file_picker/file_picker.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';

const paginaAtendimento = 'https://seusite.com/atendimento.html';

WebViewController criarController() {
  final controller = WebViewController(
    onPermissionRequest: (request) => request.grant(),
  )
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setNavigationDelegate(NavigationDelegate(
      onNavigationRequest: (req) {
        if (req.url.startsWith(paginaAtendimento)) {
          return NavigationDecision.navigate;
        }
        launchUrl(Uri.parse(req.url), mode: LaunchMode.externalApplication);
        return NavigationDecision.prevent;
      },
    ))
    ..loadRequest(Uri.parse(paginaAtendimento));

  final plataforma = controller.platform;
  if (plataforma is AndroidWebViewController) {
    plataforma.setOnShowFileSelector((params) async {
      final resultado = await FilePicker.platform.pickFiles();
      final caminho = resultado?.files.single.path;
      return caminho == null ? [] : [Uri.file(caminho).toString()];
    });
  }
  return controller;
}

Exiba com WebViewWidget(controller: criarController()). As permissões de microfone no Android e no iOS são as mesmas do React Native.

Android nativo (Kotlin)

class AtendimentoActivity : AppCompatActivity() {
    private var callbackArquivo: ValueCallback<Array<Uri>>? = null

    private val escolherArquivo =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
            callbackArquivo?.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(r.resultCode, r.data))
            callbackArquivo = null
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val webView = WebView(this)
        setContentView(webView)

        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true

        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                request.grant(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE))
            }

            override fun onShowFileChooser(
                view: WebView,
                callback: ValueCallback<Array<Uri>>,
                params: FileChooserParams,
            ): Boolean {
                callbackArquivo?.onReceiveValue(null)
                callbackArquivo = callback
                escolherArquivo.launch(params.createIntent())
                return true
            }
        }

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, req: WebResourceRequest): Boolean {
                if (req.url.host == "seusite.com") return false
                startActivity(Intent(Intent.ACTION_VIEW, req.url))
                return true
            }
        }

        webView.loadUrl("https://seusite.com/atendimento.html")
    }
}

No AndroidManifest.xml, declare android.permission.INTERNET e android.permission.RECORD_AUDIO. A liberação de áudio na WebView só funciona se o app já tiver recebido a permissão de microfone do usuário.

iOS nativo (Swift)

import UIKit
import WebKit

class AtendimentoViewController: UIViewController, WKUIDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let config = WKWebViewConfiguration()
        config.allowsInlineMediaPlayback = true

        let webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.uiDelegate = self
        view.addSubview(webView)
        webView.load(URLRequest(url: URL(string: "https://seusite.com/atendimento.html")!))
    }

    // Links que abrem em nova aba vão para o Safari
    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
                 for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        if let url = navigationAction.request.url { UIApplication.shared.open(url) }
        return nil
    }

    // Microfone para mensagem de áudio (iOS 15 ou superior)
    func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin,
                 initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType,
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        decisionHandler(.grant)
    }
}

No Info.plist, adicione NSMicrophoneUsageDescription. O seletor de arquivo do WKWebView já funciona sem código extra. Se quiser permitir foto pela câmera, adicione também NSCameraUsageDescription.

Boas práticas

  • Use a mesma chave de API do site. As conversas do app aparecem no mesmo painel.
  • Não coloque a chave em código que roda fora da página de atendimento. Ela já fica no snippet, que é público por natureza.
  • Teste envio de texto, arquivo e áudio num aparelho real antes de publicar o app.
  • Se o atendimento precisar ficar aberto ao entrar na tela, oriente a pessoa a tocar no botão do chat. O widget não abre sozinho.

Próximos passos