التكاملات

حلّ اختبارات CAPTCHA في React Native WebView باستخدام CaptchaAI

لحلّ اختبار CAPTCHA يظهر داخل صفحة محمَّلة في react-native-webview، تحتاج إلى أربع خطوات فقط: اكتشاف الأداة عند اكتمال التحميل، استخراج مفتاح الموقع من DOM، إرساله إلى CaptchaAI من خادمك الخلفي، ثم حقن الرمز المحلول في الصفحة قبل إرسال النموذج. هذا الدليل يبني هذا المسار كاملاً لـ reCAPTCHA v2 وCloudflare Turnstile.

الفكرة المحورية: كود React Native والصفحة داخل WebView يعيشان في سياقين منفصلين ويتواصلان عبر جسر رسائل واحد. سنستخدم هذا الجسر لنقل معلمات CAPTCHA إلى الطرف الأصلي، ونُبقي مفتاح الـ API على الخادم بعيداً عن نسخة التطبيق.

لماذا يظهر CAPTCHA داخل WebView أصلاً

الكثير من تطبيقات React Native لا تبني كل شاشة أصلياً؛ فهي تحمّل نماذج جهات خارجية — بوابة دفع، أو صفحة تسجيل دخول لشريك، أو نموذج حجز — داخل WebView. هذه الصفحات تفرض حمايتها الخاصة، فيظهر مربع reCAPTCHA v2 أو عنصر Cloudflare Turnstile فجأة داخل تطبيقك، ويوقف تدفق المستخدم عند الإرسال.

بما أن الصفحة ليست ملكك، لا تستطيع تعديل الـ HTML الخاص بها. لكنك تملك WebView المضيف، ويمكنك حقن JavaScript فيه لقراءة مفتاح الموقع وإعادة الرمز المحلول. هذا بالضبط ما يجعل CaptchaAI مناسباً هنا: أنت تتعامل مع اختبار CAPTCHA المطلوب برمجياً دون لمس مصدر الصفحة.

سيناريو من السوق العربي: نموذج حجز داخل التطبيق

تخيّل تطبيق حجوزات في مصر أو الخليج يفتح داخل WebView نموذجاً تابعاً لمزوّد خارجي لتأكيد الطلب. يتضمن النموذج مربع reCAPTCHA v2 يجب إكماله قبل الإرسال، وإلا يبقى المستخدم عالقاً في منتصف عملية الحجز. الهدف عملي وواضح:

  1. اكتشف أداة CAPTCHA عند انتهاء تحميل WebView
  2. استخرج مفتاح الموقع من DOM
  3. حلّه عبر CaptchaAI API من خدمة الواجهة الخلفية
  4. أدخل الرمز مرة أخرى في WebView وأرسل النموذج

البيئة: React Native 0.72+، وreact-native-webview 13+، وخادم خلفي بلغة Node.js، وCaptchaAI API.

بنية الحل عبر ثلاث طبقات

يتوزّع التدفق على ثلاث طبقات، لكل منها مسؤولية واحدة واضحة، كما يوضح الجدول التالي.

الطبقة المسؤولية
React Native WebView يكتشف اختبار CAPTCHA، ويستخرج مفتاح الموقع، ويحقن الرمز المحلول
الخادم الخلفي (Node.js) يستقبل مفتاح الموقع وعنوان الصفحة، ويستدعي CaptchaAI، ويعيد الرمز
CaptchaAI API يحلّ اختبار CAPTCHA ويعيد الرمز

يتواصل WebView مع كود React Native عبر window.ReactNativeWebView.postMessage()، بينما يتولّى الخادم الخلفي كامل التفاعل مع CaptchaAI ليبقى مفتاح الـ API خارج نسخة العميل.

الخطوة 1: اكتشاف CAPTCHA واستخراج مفتاح الموقع داخل WebView

استخدم خاصية injectedJavaScript لفحص الصفحة بحثاً عن عناصر CAPTCHA بمجرد اكتمال تحميلها، ثم أعِد النتيجة إلى الطرف الأصلي عبر الجسر:

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

يميّز السكربت بين نوعَي CAPTCHA عبر الفئة ‎.g-recaptcha أو ‎.cf-turnstile، ويرسل نوع الأداة ومفتاح الموقع وعنوان الصفحة في رسالة واحدة إلى handleMessage، الذي يستدعي الخادم الخلفي ثم يمرّر الرمز إلى injectToken.

الخطوة 2: بناء خدمة الحل في الخادم الخلفي (Node.js)

احتفظ بمفتاح CaptchaAI API على جانب الخادم دائماً. يستقبل الخادم الخلفي مفتاح الموقع وعنوان الصفحة، ويُرسل المهمة إلى CaptchaAI، ثم يستطلع النتيجة دورياً ويعيد الرمز إلى التطبيق:

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

لاحظ اختلاف اسم معلمة مفتاح الموقع بين النوعين: reCAPTCHA v2 يستخدم googlekey مع الطريقة userrecaptcha، بينما يستخدم Turnstile الحقل sitekey مع الطريقة turnstile. أمّا نقطتا النهاية in.php وres.php فمشتركتان بين النوعين.

الخطوة 3: التعامل مع انتهاء صلاحية الرمز قبل إرسال النموذج

رموز CAPTCHA لها عمر محدود: رمز reCAPTCHA v2 يبقى صالحاً نحو 120 ثانية، ورمز Turnstile نحو 300 ثانية. إذا تأخّر المستخدم بين لحظة الحل ولحظة الإرسال، فقد يرفض الموقع رمزاً منتهياً. الحل أن تُعيد الحصول على رمز جديد قبل الإرسال متى تجاوز عمر الرمز الحد الآمن:

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

التحقق من الرصيد واختيار الخطة المناسبة

قبل الإطلاق، تأكّد من وجود رصيد كافٍ على حسابك، وتحقّق من الرصيد من لوحة التحكم أو عبر الـ API حتى لا تتوقف عمليات الحل في منتصف موجة طلبات. تعتمد خطط CaptchaAI على نموذج الـ Thread المتزامن — أي عدد الطلبات قيد المعالجة في اللحظة نفسها — مع عدد حلول غير محدود لكل Thread طوال الشهر، دون رسوم لكل عملية حل:

جميع الأسعار بالدولار الأمريكي، والفوترة على أساس عدد الـ Threads المتزامنة لا على أساس كل عملية حل.

  • BASIC بسعر 15 دولاراً شهرياً مع 5 Threads — مناسب للتجربة والتطبيقات منخفضة الحركة.
  • ADVANCE بسعر 90 دولاراً شهرياً مع 50 Thread — نقطة انطلاق عملية لتطبيق جوّال متوسط الحركة.
  • PREMIUM بسعر 170 دولاراً شهرياً مع 100 Thread — لتطبيق كثيف الطلبات مع ذروات استخدام واضحة.

بما أن الفوترة على أساس الـ Thread وليس على أساس كل عملية حل، تصبح التكلفة الشهرية قابلة للتنبؤ حتى مع تذبذب أحجام الطلبات في التطبيقات الجوّالة.


معالجة الأعطال الشائعة

معظم مشكلات التكامل تعود إلى الجسر بين WebView والطرف الأصلي أو إلى معلمات غير متطابقة؛ الجدول التالي يلخّص أكثرها تكراراً مع إجراء المعالجة لكل حالة.

المشكلة السبب الإجراء
يُنشأ الرمز لكن الجهة المستهدفة ترفضه مفتاح الموقع أو الصفحة أو سياق الجلسة لا يتطابق التقط المعلمات من جديد وأعد استخدام الرمز داخل الجلسة نفسها
تنتهي عملية الاستطلاع بمهلة الفاصل الزمني أو معالجة الأخطاء صارمة أكثر من اللازم استطلع كل 5 إلى 10 ثوانٍ وافصل بين انتهاء المهلة والأخطاء الفعلية
ينجح المثال محلياً لكنه يفشل داخل التطبيق رسالة postMessage أو حقل النموذج أو حقن الرمز مفقود في السلسلة الفعلية تحقّق من المسار الكامل من الاكتشاف حتى الإرسال النهائي
يظهر عنصر CAPTCHA فارغاً في WebView JavaScript معطّل أو محتوى محجوب فعّل javaScriptEnabled={true} وتأكّد من عدم حجب سياسة أمان المحتوى للعناصر

أسئلة شائعة

هل يتعامل هذا الحل مع hCaptcha أو FunCaptcha؟

لا؛ لا يدعم CaptchaAI حالياً hCaptcha ولا FunCaptcha، لذا يقتصر هذا المسار على reCAPTCHA v2 وCloudflare Turnstile. إن كان النموذج داخل WebView يعرض أحد هذين النوعين، فلن ينجح الحقن، ويجدر التخطيط لمسار بديل في تدفق التطبيق.

ما خطة CaptchaAI المناسبة لتطبيق جوّال كثيف الطلبات؟

اختر الخطة بحسب عدد الطلبات المتزامنة المتوقعة في الذروة، لا بحسب إجمالي عدد الحلول. تطبيق متوسط الحركة يبدأ عملياً من خطة ADVANCE (90 دولاراً شهرياً، 50 Thread)، بينما يستفيد التطبيق كثيف الطلبات من PREMIUM (170 دولاراً شهرياً، 100 Thread)، مع حلول غير محدودة لكل Thread.

هل يمكنني استدعاء CaptchaAI مباشرة من التطبيق دون خادم خلفي؟

تقنياً نعم، لكن ذلك يكشف مفتاح الـ API داخل نسخة التطبيق ويسهّل استخراجه من الحزمة.

مرّر كل الطلبات عبر خادمك الخلفي كما في المثال، ليبقى المفتاح على الخادم، ولتتحكّم في حدود الاستخدام والتسجيل ومراقبة الأخطاء من مكان واحد.

كيف أتعامل مع صفحة تجمع reCAPTCHA v2 وTurnstile معاً؟

سكربت الاكتشاف يفحص النوعين بالترتيب. عند وجود أكثر من تحدٍّ في الصفحة نفسها، وسّع المسار على ثلاث مراحل:

  • اجمع كل مفاتيح المواقع الموجودة في DOM.
  • حلّ كل مفتاح تباعاً عبر الخادم الخلفي.
  • احقن كل رمز في حقله الصحيح قبل إرسال النموذج.

هل يصلح النمط نفسه لنماذج التأشيرات ومواقع BLS؟

نعم من حيث المبدأ؛ يدعم CaptchaAI اختبار BLS، ونمط الاكتشاف والحقن نفسه ينطبق داخل WebView. الفرق أن آلية اكتشاف العنصر تختلف عن reCAPTCHA، لذا عدّل محدِّد querySelector ومعلمات الإرسال بما يناسب النوع المطلوب.


الخطوات التالية

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.