diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index 66af4e9..8951a7c 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -24,7 +24,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import * as z from 'zod'; import { Captcha } from '../shared/captcha'; @@ -56,6 +56,7 @@ export const LoginForm = ({ const [success, setSuccess] = useState(''); const [isPending, setIsPending] = useState(false); const [showPassword, setShowPassword] = useState(false); + const captchaRef = useRef(null); // Check if credential login is enabled const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin; @@ -92,6 +93,15 @@ export const LoginForm = ({ name: 'captchaToken', }); + // Function to reset captcha + const resetCaptcha = () => { + form.setValue('captchaToken', ''); + // Try to reset the Turnstile widget if available + if (captchaRef.current && typeof captchaRef.current.reset === 'function') { + captchaRef.current.reset(); + } + }; + const onSubmit = async (values: z.infer) => { // Validate captcha token if turnstile is enabled and site key is available if (captchaConfigured && values.captchaToken) { @@ -107,6 +117,8 @@ export const LoginForm = ({ console.error('login, captcha invalid:', values.captchaToken); const errorMessage = captchaResult?.data?.error || t('captchaInvalid'); setError(errorMessage); + setIsPending(false); + resetCaptcha(); // Reset captcha on validation failure return; } } @@ -139,6 +151,10 @@ export const LoginForm = ({ onError: (ctx) => { console.error('login, error:', ctx.error); setError(`${ctx.error.status}: ${ctx.error.message}`); + // Reset captcha on login error + if (captchaConfigured) { + resetCaptcha(); + } }, } ); @@ -237,6 +253,7 @@ export const LoginForm = ({ {captchaConfigured && ( form.setValue('captchaToken', token)} validationError={form.formState.errors.captchaToken?.message} /> diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx index b5e7d09..d7b5d7a 100644 --- a/src/components/auth/register-form.tsx +++ b/src/components/auth/register-form.tsx @@ -22,7 +22,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import * as z from 'zod'; import { Captcha } from '../shared/captcha'; @@ -51,6 +51,7 @@ export const RegisterForm = ({ const [success, setSuccess] = useState(''); const [isPending, setIsPending] = useState(false); const [showPassword, setShowPassword] = useState(false); + const captchaRef = useRef(null); // Check if credential login is enabled const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin; @@ -91,6 +92,15 @@ export const RegisterForm = ({ name: 'captchaToken', }); + // Function to reset captcha + const resetCaptcha = () => { + form.setValue('captchaToken', ''); + // Try to reset the Turnstile widget if available + if (captchaRef.current && typeof captchaRef.current.reset === 'function') { + captchaRef.current.reset(); + } + }; + const onSubmit = async (values: z.infer) => { // Validate captcha token if turnstile is enabled and site key is available if (captchaConfigured && values.captchaToken) { @@ -106,6 +116,8 @@ export const RegisterForm = ({ console.error('register, captcha invalid:', values.captchaToken); const errorMessage = captchaResult?.data?.error || t('captchaInvalid'); setError(errorMessage); + setIsPending(false); + resetCaptcha(); // Reset captcha on validation failure return; } } @@ -148,6 +160,10 @@ export const RegisterForm = ({ // sign up fail, display the error message console.error('register, error:', ctx.error); setError(`${ctx.error.status}: ${ctx.error.message}`); + // Reset captcha on registration error + if (captchaConfigured) { + resetCaptcha(); + } }, } ); @@ -247,6 +263,7 @@ export const RegisterForm = ({ {captchaConfigured && ( form.setValue('captchaToken', token)} validationError={form.formState.errors.captchaToken?.message} /> diff --git a/src/components/shared/captcha.tsx b/src/components/shared/captcha.tsx index 542a87a..3785eaf 100644 --- a/src/components/shared/captcha.tsx +++ b/src/components/shared/captcha.tsx @@ -5,7 +5,7 @@ import { websiteConfig } from '@/config/website'; import { useLocale } from 'next-intl'; import { useTheme } from 'next-themes'; import dynamic from 'next/dynamic'; -import type { ComponentProps } from 'react'; +import { type ComponentProps, forwardRef } from 'react'; const Turnstile = dynamic( () => import('@marsidev/react-turnstile').then((mod) => mod.Turnstile), @@ -21,41 +21,46 @@ type Props = Omit, 'siteKey'> & { /** * Captcha component for Cloudflare Turnstile */ -export const Captcha = ({ validationError, ...props }: Props) => { - const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha; - const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; +export const Captcha = forwardRef( + ({ validationError, ...props }, ref) => { + const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha; + const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; - // If turnstile is disabled in config, don't render anything - if (!turnstileEnabled) { - return null; + // If turnstile is disabled in config, don't render anything + if (!turnstileEnabled) { + return null; + } + + // If turnstile is enabled but site key is missing, show error message + if (!siteKey) { + console.error('Captcha: NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set'); + return null; + } + + const theme = useTheme(); + const locale = useLocale(); + + return ( + <> + + + {validationError && ( + + {validationError} + + )} + + ); } +); - // If turnstile is enabled but site key is missing, show error message - if (!siteKey) { - console.error('Captcha: NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set'); - return null; - } - - const theme = useTheme(); - const locale = useLocale(); - - return ( - <> - - - {validationError && ( - - {validationError} - - )} - - ); -}; +Captcha.displayName = 'Captcha';