"use client"; const TURNSTILE_SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; const API_READY_TIMEOUT_MS = 15_000; const API_POLL_INTERVAL_MS = 50; export type TurnstileWidgetId = string; export type TurnstileApi = { render: ( container: HTMLElement, options: { sitekey: string; action: string; callback: (token: string) => void; "expired-callback": () => void; "error-callback": () => void; }, ) => TurnstileWidgetId; reset: (widgetId: TurnstileWidgetId) => void; }; declare global { interface Window { turnstile?: TurnstileApi; } } let turnstilePromise: Promise | null = null; function getReadyApi(): TurnstileApi | null { if (typeof window === "undefined") return null; const api = window.turnstile; if (!api || typeof api.render !== "function" || typeof api.reset !== "function") return null; return api; } function findTurnstileScript(): HTMLScriptElement | null { return ( Array.from(document.scripts).find((script) => { try { return new URL(script.src, document.baseURI).href === TURNSTILE_SCRIPT_SRC; } catch { return false; } }) ?? null ); } function waitForReadyApi(script: HTMLScriptElement): Promise { return new Promise((resolve, reject) => { let settled = false; const cleanup = () => { if (intervalId !== undefined) window.clearInterval(intervalId); if (timeoutId !== undefined) window.clearTimeout(timeoutId); script.removeEventListener("error", onError); }; const finish = (error?: Error) => { if (settled) return; settled = true; cleanup(); if (error) reject(error); else resolve(getReadyApi() as TurnstileApi); }; const check = () => { const api = getReadyApi(); if (api) finish(); }; const onError = () => { finish(new Error("Cloudflare Turnstile script failed to load")); }; const intervalId = window.setInterval(check, API_POLL_INTERVAL_MS); const timeoutId = window.setTimeout(() => { finish(new Error("Cloudflare Turnstile API did not become ready in time")); }, API_READY_TIMEOUT_MS); script.addEventListener("error", onError, { once: true }); check(); }); } /** * Loads Turnstile once and resolves only after window.turnstile.render/reset exist. * This also adopts a script already inserted by Next.js or another component. */ export function loadTurnstile(): Promise { if (typeof window === "undefined" || typeof document === "undefined") { return Promise.reject(new Error("Cloudflare Turnstile requires a browser")); } const readyApi = getReadyApi(); if (readyApi) return Promise.resolve(readyApi); if (turnstilePromise) return turnstilePromise; const script = findTurnstileScript() ?? document.createElement("script"); if (!script.src) { script.src = TURNSTILE_SCRIPT_SRC; script.async = true; script.defer = true; script.dataset.turnstileLoader = "true"; document.head.appendChild(script); } turnstilePromise = waitForReadyApi(script).catch((error: unknown) => { turnstilePromise = null; throw error; }); return turnstilePromise; }