Integrations

Solve CAPTCHA di React Native WebViews dengan CaptchaAI

Cara paling praktis untuk solve CAPTCHA di WebView React Native: biarkan WebView hanya mendeteksi widget dan menyuntik token, sementara panggilan ke CaptchaAI dijalankan dari backend Anda sendiri — karena react-native-webview sering memuat formulir pihak ketiga (checkout marketplace, portal booking) yang menampilkan reCAPTCHA v2 atau Cloudflare Turnstile dan memblokir pengiriman formulir sampai tantangan selesai. Panduan ini membahas alur lengkapnya: deteksi CAPTCHA di WebView, ekstraksi sitekey lewat injeksi JavaScript, kirim ke API CaptchaAI, lalu suntikkan token kembali ke halaman.

Arsitektur WebView React Native: Tiga Lapisan yang Saling Terhubung

Sebelum masuk ke kode, kenali dulu pembagian tanggung jawabnya — alurnya terbagi menjadi tiga lapisan:

Lapisan Tanggung Jawab
WebView React Native Mendeteksi CAPTCHA, mengambil sitekey, menyuntikkan token yang sudah selesai
Backend API (Node.js) Menerima sitekey + pageurl, memanggil CaptchaAI, mengembalikan token
API CaptchaAI Menyelesaikan CAPTCHA dan mengembalikan token

WebView berbicara dengan kode React Native Anda lewat window.ReactNativeWebView.postMessage(), sementara backend menangani interaksi CaptchaAI agar kunci API tidak menyentuh sisi klien — binari mobile relatif mudah didekompilasi.

Studi Kasus: CAPTCHA di WebView React Native yang Memblokir Formulir

Bayangkan aplikasi React Native Anda memuat formulir web pihak ketiga di dalam WebView — skenario umum bagi tim mobile Indonesia yang mengintegrasikan checkout marketplace atau portal booking mitra. Formulir itu menampilkan kotak centang reCAPTCHA v2 yang wajib diselesaikan sebelum tombol kirim aktif. Targetnya:

  1. Deteksi widget CAPTCHA begitu WebView selesai dimuat
  2. Ambil sitekey dari DOM
  3. Selesaikan lewat API CaptchaAI dari layanan backend
  4. Suntikkan token ke WebView dan kirim formulirnya
  • Lingkungan: React Native 0.72+, react-native-webview 13+, backend Node.js, API CaptchaAI
  • Expo: pola yang sama berlaku untuk workflow Expo terkelola (SDK 49+ dengan expo-dev-client) tanpa perubahan pada skrip injeksi maupun pertukaran pesan

Langkah 1: Deteksi CAPTCHA dan Ambil Sitekey di WebView

Pakai prop injectedJavaScript untuk memindai elemen CAPTCHA begitu halaman dimuat. Skrip ini mengecek dua kemungkinan — reCAPTCHA v2 lewat selector .g-recaptcha, atau Turnstile lewat .cf-turnstile — lalu mengirim hasil deteksinya ke sisi native lewat postMessage:

// 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>
  );
}

Langkah 2: Bangun Backend Solver dengan Node.js

  • Simpan kunci API CaptchaAI hanya di server — jangan pernah taruh di kode React Native
  • Backend menerima sitekey + pageurl dari WebView, lalu mengirim task ke CaptchaAI
  • Backend polling sampai hasil siap, lalu mengembalikan token ke aplikasi
// 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'));

Langkah 3: Tangani Token yang Kedaluwarsa

  • Token reCAPTCHA v2 bertahan sekitar 120 detik, token Turnstile 300 detik
  • Solve reCAPTCHA v2 biasanya makan waktu 10-20 detik, Turnstile 5-15 detik — tampilkan indikator loading selama jendela itu
  • Jika pengguna menunda submit (umum di jaringan seluler lambat), selesaikan ulang CAPTCHA sebelum submit agar token tidak ditolak server tujuan
// 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;
  `);
};

Kendala yang Sering Muncul

  • postMessage tidak sampai — penyebab: onMessage di WebView belum terpasang atau skrip injeksi error. Solusi: pastikan onMessage sudah di-bind dan bungkus skrip injeksi dengan try/catch.
  • ERROR_BAD_TOKEN_OR_PAGEURL — penyebab: sitekey tidak cocok dengan URL halaman. Solusi: ambil sitekey dari src iframe yang sebenarnya, bukan dari halaman induk.
  • Token tersuntik tapi callback tidak jalan — penyebab: callback reCAPTCHA tidak ketemu di ___grecaptcha_cfg. Solusi: iterasi semua objek client dan periksa properti bersarang untuk fungsi callback.
  • CAPCHA_NOT_READY terus-menerus — penyebab: solve lambat atau parameter salah. Solusi: perpanjang timeout polling dan cek ulang sitekey/pageurl.
  • WebView menampilkan CAPTCHA kosong — penyebab: JavaScript nonaktif atau konten diblokir kebijakan keamanan. Solusi: set javaScriptEnabled={true} dan pastikan tidak ada content security policy yang memblokir.

Mulai Sekarang

Mulai selesaikan CAPTCHA di aplikasi React Native Anda — dapatkan kunci API CaptchaAI Anda dan integrasikan backend solver-nya hari ini.

Pertanyaan Umum

Berapa biaya CaptchaAI untuk aplikasi React Native dengan trafik tinggi?

CaptchaAI memakai harga berbasis thread, bukan per solve — solve tak terbatas selama thread tersedia. BASIC ($15/bulan, 5 thread) cukup untuk uji coba; kalau volume naik, upgrade ke ADVANCE ($90/bulan, 50 thread) atau PREMIUM ($170/bulan, 100 thread) tanpa biaya tambahan per CAPTCHA.

Amankah memanggil API CaptchaAI langsung dari kode React Native tanpa backend?

Secara teknis bisa, tapi kunci API Anda ikut ter-bundle di biner aplikasi dan bisa diekstrak lewat dekompilasi APK/IPA. Selalu rutekan panggilan lewat backend sendiri seperti pada Langkah 2.

Apakah CaptchaAI juga bisa menyelesaikan hCaptcha di dalam WebView?

Belum. hCaptcha tidak didukung CaptchaAI saat ini — pola di panduan ini khusus untuk reCAPTCHA v2 dan Cloudflare Turnstile. Untuk halaman yang memakai hCaptcha, Anda perlu layanan lain.

Apakah pola yang sama berlaku untuk WebView di Flutter atau native Android/iOS?

Konsepnya sama: deteksi sitekey, kirim ke backend, suntik token — hanya jembatan JavaScript-ke-native yang beda: postMessage di Flutter WebView, atau evaluateJavascript/WKUserContentController di kode native Android/iOS.

Bacaan Lanjutan

Komentar dinonaktifkan untuk artikel ini.