import { useCallback, useEffect, useRef, useState } from "react"; const SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; type TurnstileApi = { render: ( element: HTMLElement, options: { sitekey: string; action: string; theme?: "light" | "dark" | "auto"; callback: (token: string) => void; "expired-callback": () => void; "error-callback": () => void; }, ) => string; reset: (widgetId: string) => void; remove: (widgetId: string) => void; }; declare global { interface Window { turnstile?: TurnstileApi; } } let loader: Promise | null = null; function loadTurnstile(): Promise { if (window.turnstile) return Promise.resolve(window.turnstile); loader ??= new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = SCRIPT_SRC; script.async = true; script.defer = true; script.onload = () => window.turnstile ? resolve(window.turnstile) : reject(new Error("Turnstile failed to load")); script.onerror = () => { loader = null; reject(new Error("Turnstile failed to load")); }; document.head.appendChild(script); }); return loader; } /** * Explicitly rendered Turnstile widget for an SPA form. Tokens are single-use, so call `reset` after every * submit attempt; the widget issues a fresh token before the next one. A null sitekey disables it. */ export function useTurnstile(siteKey: string | null, action: string) { const container = useRef(null); const widgetId = useRef(null); const [token, setToken] = useState(null); const [failed, setFailed] = useState(false); useEffect(() => { if (!siteKey || !container.current) return; let cancelled = false; loadTurnstile() .then((api) => { if (cancelled || !container.current) return; widgetId.current = api.render(container.current, { sitekey: siteKey, action, theme: "dark", callback: (value) => { setFailed(false); setToken(value); }, "expired-callback": () => setToken(null), "error-callback": () => { setToken(null); setFailed(true); }, }); }) .catch(() => setFailed(true)); return () => { cancelled = true; if (widgetId.current) window.turnstile?.remove(widgetId.current); widgetId.current = null; }; }, [siteKey, action]); const reset = useCallback(() => { setToken(null); if (widgetId.current) window.turnstile?.reset(widgetId.current); }, []); return { enabled: Boolean(siteKey), /** Undefined when Turnstile is off, so the server field is simply omitted. */ token: siteKey ? (token ?? "") : undefined, ready: !siteKey || Boolean(token), failed, reset, widget: siteKey ?
: null, }; }