import React, { useEffect, useRef, forwardRef, useImperativeHandle, } from "react"; interface CustomReCaptchaProps { siteKey: string; onVerify: (token: string) => void; onExpired?: () => void; onError?: () => void; className?: string; theme?: "light" | "dark"; size?: "normal" | "compact"; } export interface ReCaptchaRef { reset: () => void; execute: () => void; } declare global { interface Window { grecaptcha: any; onReCaptchaLoad?: () => void; } } const CustomReCaptcha = forwardRef( ( { siteKey, onVerify, onExpired, onError, className = "", theme = "dark", size = "normal", }, ref ) => { const captchaRef = useRef(null); const widgetId = useRef(null); const isLoadedRef = useRef(false); useImperativeHandle(ref, () => ({ reset: () => { if (window.grecaptcha && widgetId.current !== null) { window.grecaptcha.reset(widgetId.current); } }, execute: () => { if (window.grecaptcha && widgetId.current !== null) { window.grecaptcha.execute(widgetId.current); } }, })); const loadReCaptcha = () => { if (window.grecaptcha) { renderReCaptcha(); return; } // Check if script is already loading if ( document.querySelector( 'script[src^="https://www.google.com/recaptcha/api.js"]' ) ) { // If already loading, set up a callback for when it loads window.onReCaptchaLoad = renderReCaptcha; return; } // Load reCAPTCHA script const script = document.createElement("script"); script.src = "https://www.google.com/recaptcha/api.js?onload=onReCaptchaLoad&render=explicit"; script.async = true; script.defer = true; // Set up callback for when script loads window.onReCaptchaLoad = () => { renderReCaptcha(); window.onReCaptchaLoad = undefined; // Clean up }; script.onerror = () => { console.error("Failed to load reCAPTCHA script"); if (onError) onError(); window.onReCaptchaLoad = undefined; // Clean up }; document.head.appendChild(script); }; const renderReCaptcha = () => { if (!captchaRef.current || isLoadedRef.current) return; try { widgetId.current = window.grecaptcha.render(captchaRef.current, { sitekey: siteKey, callback: onVerify, "expired-callback": onExpired, "error-callback": onError, theme: theme, size: size, }); isLoadedRef.current = true; } catch (error) { console.error("Error rendering reCAPTCHA:", error); if (onError) onError(); } }; useEffect(() => { loadReCaptcha(); return () => { // Cleanup if (window.grecaptcha && widgetId.current !== null) { try { window.grecaptcha.reset(widgetId.current); } catch (error) { // Ignore cleanup errors } } // Clean up the global callback window.onReCaptchaLoad = undefined; }; }, []); // Add styles to document head instead of using styled-jsx useEffect(() => { const styleId = "custom-recaptcha-styles"; // Check if styles are already added if (document.getElementById(styleId)) { return; } const styleElement = document.createElement("style"); styleElement.id = styleId; styleElement.textContent = ` .grecaptcha-badge { visibility: hidden; } iframe[src*="recaptcha"] { border-radius: 8px !important; overflow: hidden; } .g-recaptcha { transform: scale(1); transform-origin: center; } .g-recaptcha > div { border-radius: 8px !important; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } `; document.head.appendChild(styleElement); // Cleanup function to remove styles when component unmounts return () => { const existingStyle = document.getElementById(styleId); if (existingStyle) { document.head.removeChild(existingStyle); } }; }, []); return (
); } ); CustomReCaptcha.displayName = "CustomReCaptcha"; export default CustomReCaptcha;