Integrations

Resolvendo CAPTCHAs em React Native WebViews com CaptchaAI

Para resolver um CAPTCHA dentro de um WebView do React Native você precisa de três peças funcionando juntas: um script injetado que detecta o widget e extrai a sitekey, um backend que envia essa sitekey para a CaptchaAI e devolve o token, e uma segunda injeção de JavaScript que coloca esse token de volta no formulário antes do envio.

O react-native-webview só renderiza a página — quem resolve o CAPTCHA é a API.

Este guia cobre a implementação completa: detecção no WebView, backend Node.js que fala com a CaptchaAI e reinjeção do token, incluindo o que fazer quando o token expira antes de o usuário terminar de preencher o formulário.

Como as três camadas conversam entre si

A solução se divide em três camadas independentes:

  • WebView do React Native — detecta o CAPTCHA, extrai a sitekey, injeta o token resolvido.
  • API de backend (Node.js) — recebe sitekey + pageurl, chama a CaptchaAI, devolve o token.
  • API da CaptchaAI — resolve o CAPTCHA e retorna o token.

O WebView troca mensagens com o código React Native via window.ReactNativeWebView.postMessage(). Todo o contato com a CaptchaAI acontece no backend — nunca dentro do app —, o que mantém a chave de API fora do binário distribuído nas lojas.

Se boa parte do seu público estiver no Brasil, rode esse backend em uma região próxima, como sa-east-1 da AWS: menos RTT reduz o tempo percebido na tela de carregamento. Ao registrar sitekey e pageurl nos logs, trate esses dados sob as obrigações da LGPD já válidas para o resto do app.

Quando o CAPTCHA aparece no WebView do React Native

Um exemplo concreto: um app React Native carrega, dentro de um WebView, um formulário de terceiro com um checkbox reCAPTCHA v2 que precisa ser marcado antes do envio. O fluxo a construir é:

  • detectar o widget assim que o WebView terminar de carregar;
  • extrair a sitekey diretamente do DOM;
  • resolvê-lo via API da CaptchaAI, chamada a partir do backend;
  • injetar o token de volta no WebView e disparar o envio do formulário.

Ambiente usado neste guia: React Native 0.72+, react-native-webview 13+, backend em Node.js e a API da CaptchaAI.

Passo 1: detecte o CAPTCHA e extraia a sitekey no WebView

Use a prop injectedJavaScript para varrer a página em busca de elementos CAPTCHA assim que o carregamento terminar:

// CaptchaDetector.js — React Native Component
import React, { useRef, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';

const CAPTCHA_DETECTION_SCRIPT = `
  (function() {
    // Detect reCAPTCHA v2
    const recaptchaDiv = document.querySelector('.g-recaptcha');
    if (recaptchaDiv) {
      const sitekey = recaptchaDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'recaptcha_v2',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    // Detect Cloudflare Turnstile
    const turnstileDiv = document.querySelector('.cf-turnstile');
    if (turnstileDiv) {
      const sitekey = turnstileDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'turnstile',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    window.ReactNativeWebView.postMessage(JSON.stringify({
      type: 'no_captcha'
    }));
  })();
  true;
`;

export default function CaptchaWebView({ url }) {
  const webviewRef = useRef(null);
  const [solving, setSolving] = useState(false);

  const handleMessage = async (event) => {
    const data = JSON.parse(event.nativeEvent.data);

    if (data.type === 'captcha_detected') {
      setSolving(true);
      try {
        const token = await solveCaptchaViaBackend(
          data.captchaType,
          data.sitekey,
          data.pageurl
        );
        injectToken(data.captchaType, token);
      } catch (err) {
        console.error('CAPTCHA solve failed:', err.message);
      } finally {
        setSolving(false);
      }
    }
  };

  const solveCaptchaViaBackend = async (captchaType, sitekey, pageurl) => {
    const response = await fetch('https://your-backend.com/api/solve-captcha', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ captchaType, sitekey, pageurl }),
    });
    const result = await response.json();
    if (!result.token) throw new Error(result.error || 'No token returned');
    return result.token;
  };

  const injectToken = (captchaType, token) => {
    let script;
    if (captchaType === 'recaptcha_v2') {
      script = `
        document.getElementById('g-recaptcha-response').value = '${token}';
        if (typeof ___grecaptcha_cfg !== 'undefined') {
          Object.keys(___grecaptcha_cfg.clients).forEach(key => {
            const client = ___grecaptcha_cfg.clients[key];
            Object.keys(client).forEach(k => {
              const item = client[k];
              if (item && item.callback) {
                item.callback('${token}');
              }
            });
          });
        }
        true;
      `;
    } else if (captchaType === 'turnstile') {
      script = `
        const input = document.querySelector('[name="cf-turnstile-response"]');
        if (input) input.value = '${token}';
        const callback = document.querySelector('.cf-turnstile')
          ?.getAttribute('data-callback');
        if (callback && typeof window[callback] === 'function') {
          window[callback]('${token}');
        }
        true;
      `;
    }
    webviewRef.current?.injectJavaScript(script);
  };

  return (
    <View style={{ flex: 1 }}>
      {solving && <ActivityIndicator size="large" />}
      <WebView
        ref={webviewRef}
        source={{ uri: url }}
        injectedJavaScript={CAPTCHA_DETECTION_SCRIPT}
        onMessage={handleMessage}
        javaScriptEnabled={true}
      />
    </View>
  );
}

Passo 2: monte o solver de backend em Node.js

Mantenha a chave de API da CaptchaAI apenas no servidor. O backend recebe a sitekey e a pageurl, envia a tarefa para a CaptchaAI, faz o polling do resultado e devolve o token pronto:

// server.js — Express backend
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY || 'YOUR_API_KEY';

app.post('/api/solve-captcha', async (req, res) => {
  const { captchaType, sitekey, pageurl } = req.body;

  try {
    // Step 1: Submit task to CaptchaAI
    const submitParams = {
      key: API_KEY,
      pageurl: pageurl,
      json: '1',
    };

    if (captchaType === 'recaptcha_v2') {
      submitParams.method = 'userrecaptcha';
      submitParams.googlekey = sitekey;
    } else if (captchaType === 'turnstile') {
      submitParams.method = 'turnstile';
      submitParams.sitekey = sitekey;
    }

    const submitResponse = await axios.get(
      'https://ocr.captchaai.com/in.php',
      { params: submitParams }
    );

    if (submitResponse.data.status !== 1) {
      return res.status(400).json({ error: submitResponse.data.request });
    }

    const taskId = submitResponse.data.request;

    // Step 2: Poll for result
    const token = await pollForResult(taskId);
    res.json({ token });
  } catch (error) {
    console.error('Solve error:', error.message);
    res.status(500).json({ error: 'Failed to solve CAPTCHA' });
  }
});

async function pollForResult(taskId, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const response = await axios.get('https://ocr.captchaai.com/res.php', {
      params: {
        key: API_KEY,
        action: 'get',
        id: taskId,
        json: '1',
      },
    });

    if (response.data.status === 1) {
      return response.data.request;
    }

    if (
      response.data.request !== 'CAPCHA_NOT_READY' &&
      response.data.status === 0
    ) {
      throw new Error(response.data.request);
    }
  }
  throw new Error('Polling timeout — CAPTCHA not solved in time');
}

app.listen(3000, () => console.log('Solver backend running on port 3000'));

Passo 3: trate a expiração do token antes de enviar o formulário

Tokens de CAPTCHA têm validade curta — o token do reCAPTCHA v2 dura cerca de 120 segundos, o do Turnstile cerca de 300 segundos. Se o usuário demorar para concluir o formulário, resolva de novo antes do envio:

// Add to CaptchaWebView component
const [tokenTimestamp, setTokenTimestamp] = useState(null);
const TOKEN_TTL_MS = 110000; // 110 seconds for reCAPTCHA v2

const handleFormSubmit = async (captchaType, sitekey, pageurl) => {
  const now = Date.now();
  if (!tokenTimestamp || now - tokenTimestamp > TOKEN_TTL_MS) {
    const freshToken = await solveCaptchaViaBackend(
      captchaType, sitekey, pageurl
    );
    injectToken(captchaType, freshToken);
    setTokenTimestamp(Date.now());
  }

  webviewRef.current?.injectJavaScript(`
    document.querySelector('form').submit();
    true;
  `);
};

Perguntas frequentes

É seguro chamar a API da CaptchaAI direto do app, sem passar por um backend?

Tecnicamente dá, mas a chave de API ficaria exposta dentro do binário do app — qualquer pessoa consegue extrair strings de um APK ou IPA. Roteie a chamada pelo seu próprio backend e mantenha a chave só do lado do servidor.

Funciona com o fluxo gerenciado do Expo?

Sim. O react-native-webview é suportado a partir do Expo SDK 49+ com o expo-dev-client, e a injeção de JavaScript com passagem de mensagens funciona do mesmo jeito que em um projeto React Native puro.

Preciso hospedar o backend perto do Brasil para não perder tempo de resolução?

A latência entre o dispositivo e o backend soma-se ao tempo de resolução da CaptchaAI. Uma região como sa-east-1 (São Paulo) reduz essa parcela de RTT quando a maioria dos usuários está no Brasil; para uma base global, considere réplicas por região.

Quantas threads da CaptchaAI eu preciso para um app com muitos usuários resolvendo CAPTCHA ao mesmo tempo?

Depende do pico de resoluções simultâneas, não do total de usuários do app. Cada thread processa um CAPTCHA por vez e fica livre quando o desafio termina; o BASIC (US$ 15/mês, 5 threads) cobre picos pequenos, e o STANDARD (US$ 30/mês, 15 threads) ou o ADVANCE (US$ 90/mês, 50 threads) absorvem lançamentos com mais tráfego simultâneo.

O que acontece se o app for para segundo plano enquanto o CAPTCHA está sendo resolvido?

O sistema pode pausar o WebView e a chamada de rede quando o app perde o foco. Use um timeout de polling curto e, ao voltar ao primeiro plano, verifique se o token ainda está dentro da janela de validade antes do envio — se tiver expirado, resolva de novo.

Erros comuns e como corrigir

postMessage nunca chega

O WebView não tem onMessage definido, ou o script injetado quebrou no meio da execução. Confirme que onMessage está vinculado ao componente e envolva o script de injeção em try/catch.

ERROR_BAD_TOKEN_OR_PAGEURL

A sitekey não corresponde à pageurl enviada. Extraia a sitekey do src real do iframe, não da página que o contém.

Token injetado não dispara o callback

O callback do reCAPTCHA não foi encontrado em ___grecaptcha_cfg. Percorra todos os objetos de client e verifique as propriedades aninhadas em busca da função de callback.

CAPCHA_NOT_READY sem parar

Resolução lenta ou parâmetros inválidos. Aumente o timeout do polling e confira se a sitekey e a pageurl estão corretas.

WebView mostra o CAPTCHA em branco

JavaScript desativado ou conteúdo bloqueado. Defina javaScriptEnabled={true} e verifique se não há política de segurança de conteúdo bloqueando o widget.

Continue por aqui

Comece a resolver CAPTCHAs no seu app React Native — crie sua chave de API da CaptchaAI e integre o solver de backend ainda hoje.

Os comentários estão desativados para este artigo.