'use client' import Script from 'next/script' import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react' import { useTranslations } from 'next-intl' import { getTurnstileRolloutState, resolveTurnstileSiteKey, } from '@/lib/auth/turnstile' const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit' export type TurnstileAction = | 'accounted_login' | 'accounted_password_reset' | 'accounted_signup' | 'accounted_sandbox' type TurnstileWidgetState = 'loading' | 'ready' | 'verified' | 'error' type TurnstileRenderOptions = { sitekey: string action: TurnstileAction appearance: 'interaction-only' execution: 'render' size: 'compact' theme: 'auto' callback: (token: string) => void 'error-callback': () => void 'expired-callback': () => void 'timeout-callback': () => void } type TurnstileApi = { render: ( container: HTMLElement, options: TurnstileRenderOptions, ) => string | undefined remove: (widgetId: string) => void reset: (widgetId: string) => void } declare global { interface Window { turnstile?: TurnstileApi } } export type TurnstileChallengeHandle = { reset: () => void } type TurnstileChallengeProps = { action: TurnstileAction onTokenChange: (token: string | null) => void } export const TurnstileChallenge = forwardRef< TurnstileChallengeHandle, TurnstileChallengeProps >(function TurnstileChallenge({ action, onTokenChange }, ref) { const t = useTranslations('auth') const siteKey = resolveTurnstileSiteKey() const rolloutState = getTurnstileRolloutState() const containerRef = useRef(null) const widgetIdRef = useRef(null) const onTokenChangeRef = useRef(onTokenChange) const [widgetState, setWidgetState] = useState('loading') useEffect(() => { onTokenChangeRef.current = onTokenChange }, [onTokenChange]) const clearToken = useCallback((state: TurnstileWidgetState) => { onTokenChangeRef.current(null) setWidgetState(state) }, []) const reset = useCallback(() => { clearToken('ready') const widgetId = widgetIdRef.current if (!widgetId || !window.turnstile) return try { window.turnstile.reset(widgetId) } catch { clearToken('error') } }, [clearToken]) useImperativeHandle(ref, () => ({ reset }), [reset]) const renderWidget = useCallback(() => { if (!siteKey || !containerRef.current || !window.turnstile) return if (widgetIdRef.current) return try { setWidgetState('ready') const widgetId = window.turnstile.render(containerRef.current, { sitekey: siteKey, action, appearance: 'interaction-only', execution: 'render', size: 'compact', theme: 'auto', callback: (token) => { onTokenChangeRef.current(token) setWidgetState('verified') }, 'error-callback': () => clearToken('error'), 'expired-callback': reset, 'timeout-callback': reset, }) if (!widgetId) { clearToken('error') return } widgetIdRef.current = widgetId } catch { clearToken('error') } }, [action, clearToken, reset, siteKey]) useEffect(() => { return () => { const widgetId = widgetIdRef.current if (!widgetId || !window.turnstile) return try { window.turnstile.remove(widgetId) } catch { // The provider may already have removed an expired widget. } widgetIdRef.current = null } }, []) if (!siteKey) { return (