"use client" import { useEffect, useRef } from "react" import { cn } from "@/lib/utils" declare global { interface Window { turnstile?: { render: (el: HTMLElement, opts: Record) => string remove: (id: string) => void reset: (id?: string) => void } } } const SCRIPT_ID = "cf-turnstile-script" const SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" /** * Cloudflare Turnstile widget (explicit render). * * Renders inside the enclosing
; on success Turnstile injects a hidden * that is submitted with the form and * verified server-side by verifyTurnstile(). Renders nothing when the public * site key is not configured, so the forms still work without Turnstile. */ export function TurnstileWidget({ className }: { className?: string }) { const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY const containerRef = useRef(null) const widgetIdRef = useRef(null) useEffect(() => { if (!siteKey) return let cancelled = false const render = () => { if (cancelled || widgetIdRef.current || !containerRef.current || !window.turnstile) return widgetIdRef.current = window.turnstile.render(containerRef.current, { sitekey: siteKey, theme: "dark", }) } if (!document.getElementById(SCRIPT_ID)) { const script = document.createElement("script") script.id = SCRIPT_ID script.src = SCRIPT_SRC script.async = true script.defer = true script.onload = render document.head.appendChild(script) } else { render() } // Fallback: the script may already be cached/loaded so onload won't fire. const poll = window.setInterval(() => { if (window.turnstile) { render() window.clearInterval(poll) } }, 150) return () => { cancelled = true window.clearInterval(poll) if (widgetIdRef.current && window.turnstile) { try { window.turnstile.remove(widgetIdRef.current) } catch { // widget already removed } widgetIdRef.current = null } } }, [siteKey]) if (!siteKey) return null return
}