"use client"; import Script from "next/script"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; interface TurnstileRenderOptions { sitekey: string; action?: string; callback: (token: string) => void; "error-callback"?: () => void; "expired-callback"?: () => void; } declare global { interface Window { turnstile?: { render: (container: HTMLElement, options: TurnstileRenderOptions) => string; reset: (widgetId?: string) => void; remove: (widgetId?: string) => void; }; } } export interface TurnstileWidgetHandle { /** Tokens are single-use - call this after any failed submission before * retrying, or the retry is rejected as timeout-or-duplicate. */ reset: () => void; } interface TurnstileWidgetProps { /** Sourced live from useSystemStatus()'s turnstile_site_key - not a * build-time env var, so an admin-updated key (see the Settings page's * Server secrets box) takes effect without rebuilding the frontend. */ siteKey: string | null | undefined; onToken: (token: string) => void; className?: string; } export const TurnstileWidget = forwardRef( function TurnstileWidget({ siteKey, onToken, className }, ref) { const containerRef = useRef(null); const widgetIdRef = useRef(null); const onTokenRef = useRef(onToken); const [scriptLoaded, setScriptLoaded] = useState( () => typeof window !== "undefined" && !!window.turnstile, ); useEffect(() => { onTokenRef.current = onToken; }, [onToken]); // next/script's onLoad only reliably fires for the mount that actually // injects the tag - a later page that mounts this same widget after an // earlier page already loaded the Cloudflare script (same src, so Next // skips re-injecting it) can otherwise wait on an onLoad that never // comes. Poll briefly as a fallback for that case. useEffect(() => { if (scriptLoaded) return; const interval = setInterval(() => { if (window.turnstile) { setScriptLoaded(true); clearInterval(interval); } }, 100); return () => clearInterval(interval); }, [scriptLoaded]); useImperativeHandle(ref, () => ({ reset: () => { if (window.turnstile && widgetIdRef.current) { window.turnstile.reset(widgetIdRef.current); } }, })); useEffect(() => { if (!scriptLoaded || !containerRef.current || !window.turnstile || !siteKey) return; // Explicit render, not implicit auto-render, into our own ref'd div. const widgetId = window.turnstile.render(containerRef.current, { sitekey: siteKey, action: "turnstile-spin-v2", callback: (token) => onTokenRef.current(token), }); widgetIdRef.current = widgetId; return () => { window.turnstile?.remove(widgetId); widgetIdRef.current = null; }; }, [scriptLoaded, siteKey]); if (!siteKey) return null; return ( <>