UNPKG

@valture/react-native-recaptcha-v3

Version:

React Native component for integrating Google reCAPTCHA v3, providing seamless bot protection for mobile applications.

231 lines (222 loc) 8 kB
"use strict"; import React, { forwardRef, useImperativeHandle, useRef, useState, useEffect } from 'react'; import { StyleSheet } from 'react-native'; import { WebView } from 'react-native-webview'; import { jsx as _jsx } from "react/jsx-runtime"; const ReCaptchaV3 = /*#__PURE__*/forwardRef(({ siteKey = 'dummy-site-key', baseUrl = 'https://example.com', action = 'submit', onVerify, onError, containerStyle, style }, ref) => { const webViewRef = useRef(null); const [isReady, setIsReady] = useState(false); const tokenPromiseRef = useRef(null); // Queue for token requests that come in before the reCAPTCHA is ready const pendingRequests = useRef([]); const handleError = React.useCallback(error => { onError?.(error); if (tokenPromiseRef.current) { tokenPromiseRef.current.reject(new Error(error)); tokenPromiseRef.current = null; } }, [onError]); // Handle the case when reCAPTCHA is ready useEffect(() => { if (isReady && pendingRequests.current.length > 0) { console.log(`Processing ${pendingRequests.current.length} pending reCAPTCHA requests`); // Process all pending requests const requests = [...pendingRequests.current]; pendingRequests.current = []; // Process the first request immediately const firstRequest = requests.shift(); if (firstRequest) { executeReCaptcha(firstRequest.action, firstRequest.resolve, firstRequest.reject); } // Queue the rest with a small delay to prevent overwhelming the WebView requests.forEach((request, index) => { setTimeout(() => { executeReCaptcha(request.action, request.resolve, request.reject); }, (index + 1) * 500); // Stagger requests by 500ms }); } }, [isReady]); // Function to execute reCAPTCHA and get a token const executeReCaptcha = (customAction, resolve, reject) => { if (!isReady) { console.warn('reCAPTCHA not ready yet, queueing request'); pendingRequests.current.push({ action: customAction, resolve, reject }); return; } tokenPromiseRef.current = { resolve, reject, action: customAction }; const jsToInject = ` (function executeReCaptcha() { try { if (window.grecaptcha && window.grecaptcha.execute) { window.grecaptcha.execute('${siteKey}', { action: '${customAction}' }) .then(function(token) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'VERIFY', token: token, action: '${customAction}' })); }) .catch(function(error) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ERROR', error: error.message || 'reCAPTCHA execution failed', action: '${customAction}' })); }); } else { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ERROR', error: 'reCAPTCHA not ready', action: '${customAction}' })); } } catch (e) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ERROR', error: 'JavaScript execution error: ' + (e.message || 'Unknown error'), action: '${customAction}' })); } return true; })(); `; webViewRef.current?.injectJavaScript(jsToInject); }; useImperativeHandle(ref, () => ({ getToken: (customAction = action) => { console.log("🚀 ~ useImperativeHandle ~ customAction:", customAction); return new Promise((resolve, reject) => { executeReCaptcha(customAction, resolve, reject); }); }, // Add a new method to check if reCAPTCHA is ready isReady: () => { return isReady; } }), [siteKey, action, isReady]); const htmlContent = ` <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script> // Global error handler window.onerror = function(msg, url, lineNo, columnNo, error) { if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ERROR', error: 'Script error: ' + msg })); } return false; }; // Function to check if reCAPTCHA is ready and notify the React Native app function checkRecaptchaReady() { if (window.grecaptcha && window.grecaptcha.ready) { window.grecaptcha.ready(function() { if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'READY' })); } }); } else { setTimeout(checkRecaptchaReady, 500); } } // Start checking for reCAPTCHA readiness immediately setTimeout(checkRecaptchaReady, 500); </script> </head> <body style="background-color: transparent;"> <div id="recaptcha-container"></div> <script src="https://www.google.com/recaptcha/api.js?render=${siteKey}" async defer></script> <script> // Also check readiness when DOM is fully loaded document.addEventListener('DOMContentLoaded', function() { checkRecaptchaReady(); }); </script> </body> </html> `; const handleMessage = React.useCallback(event => { try { const data = JSON.parse(event.nativeEvent.data); if (data.type === 'READY') { console.log('reCAPTCHA is ready'); setIsReady(true); } else if (data.type === 'VERIFY' && data.token) { console.log('reCAPTCHA token received'); onVerify?.(data.token); if (tokenPromiseRef.current) { tokenPromiseRef.current.resolve(data.token); tokenPromiseRef.current = null; } } else if (data.type === 'ERROR') { console.warn('reCAPTCHA error:', data.error); handleError(data.error || 'reCAPTCHA error'); } } catch (error) { console.error('Failed to parse WebView message:', error); handleError('Failed to parse reCAPTCHA response'); } }, [onVerify, handleError]); const handleWebViewError = React.useCallback(syntheticEvent => { const { nativeEvent } = syntheticEvent; console.error('WebView error:', nativeEvent); handleError(`WebView error: ${nativeEvent.description}`); }, [handleError]); const handleWebViewHttpError = React.useCallback(syntheticEvent => { const { nativeEvent } = syntheticEvent; console.error('WebView HTTP error:', nativeEvent); handleError(`WebView HTTP error: ${nativeEvent.statusCode}`); }, [handleError]); return /*#__PURE__*/_jsx(WebView, { ref: webViewRef, source: { html: htmlContent, baseUrl }, onMessage: handleMessage, javaScriptEnabled: true, domStorageEnabled: true, originWhitelist: ['*'], style: [styles.webview, style], automaticallyAdjustContentInsets: true, mixedContentMode: 'always', containerStyle: [styles.container, containerStyle], onError: handleWebViewError, onHttpError: handleWebViewHttpError }); }); const styles = StyleSheet.create({ container: { justifyContent: 'center' }, webview: { backgroundColor: 'transparent' } }); export default ReCaptchaV3; //# sourceMappingURL=index.js.map