Captcha Refactoring (#397)

* refactor: replace useCaptchaToken with useCaptcha hook and integrate CaptchaField across forms
This commit is contained in:
Giancarlo Buomprisco
2025-10-21 20:46:35 +09:00
committed by GitHub
parent 9eccb319af
commit ea0c1dde80
17 changed files with 303 additions and 178 deletions

View File

@@ -23,7 +23,7 @@ import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import { TermsAndConditionsFormField } from './terms-and-conditions-form-field';
@@ -32,16 +32,18 @@ export function MagicLinkAuthContainer({
shouldCreateUser,
defaultValues,
displayTermsCheckbox,
captchaSiteKey,
}: {
redirectUrl: string;
shouldCreateUser: boolean;
displayTermsCheckbox?: boolean;
captchaSiteKey?: string;
defaultValues?: {
email: string;
};
}) {
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
const captcha = useCaptcha({ siteKey: captchaSiteKey });
const { t } = useTranslation();
const signInWithOtpMutation = useSignInWithOtp();
const appEvents = useAppEvents();
@@ -68,7 +70,7 @@ export function MagicLinkAuthContainer({
email,
options: {
emailRedirectTo,
captchaToken,
captchaToken: captcha.token,
shouldCreateUser,
},
});
@@ -91,7 +93,7 @@ export function MagicLinkAuthContainer({
error: t(`auth:errors.linkTitle`),
});
resetCaptchaToken();
captcha.reset();
};
if (signInWithOtpMutation.data) {
@@ -106,6 +108,8 @@ export function MagicLinkAuthContainer({
<ErrorAlert />
</If>
{captcha.field}
<FormField
render={({ field }) => (
<FormItem>

View File

@@ -27,7 +27,7 @@ import {
import { Spinner } from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import { AuthErrorAlert } from './auth-error-alert';
@@ -36,6 +36,7 @@ const OtpSchema = z.object({ token: z.string().min(6).max(6) });
type OtpSignInContainerProps = {
shouldCreateUser: boolean;
captchaSiteKey?: string;
};
export function OtpSignInContainer(props: OtpSignInContainerProps) {
@@ -88,6 +89,7 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
return (
<OtpEmailForm
shouldCreateUser={shouldCreateUser}
captchaSiteKey={props.captchaSiteKey}
onSendOtp={(email) => {
otpForm.setValue('email', email, {
shouldValidate: true,
@@ -174,12 +176,14 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
function OtpEmailForm({
shouldCreateUser,
captchaSiteKey,
onSendOtp,
}: {
shouldCreateUser: boolean;
captchaSiteKey?: string;
onSendOtp: (email: string) => void;
}) {
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
const captcha = useCaptcha({ siteKey: captchaSiteKey });
const signInMutation = useSignInWithOtp();
const emailForm = useForm({
@@ -190,10 +194,10 @@ function OtpEmailForm({
const handleSendOtp = async ({ email }: z.infer<typeof EmailSchema>) => {
await signInMutation.mutateAsync({
email,
options: { captchaToken, shouldCreateUser },
options: { captchaToken: captcha.token, shouldCreateUser },
});
resetCaptchaToken();
captcha.reset();
onSendOtp(email);
};
@@ -205,6 +209,8 @@ function OtpEmailForm({
>
<AuthErrorAlert error={signInMutation.error} />
{captcha.field}
<FormField
name="email"
render={({ field }) => (

View File

@@ -20,7 +20,7 @@ import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
import { AuthErrorAlert } from './auth-error-alert';
const PasswordResetSchema = z.object({
@@ -29,10 +29,11 @@ const PasswordResetSchema = z.object({
export function PasswordResetRequestContainer(params: {
redirectPath: string;
captchaSiteKey?: string;
}) {
const { t } = useTranslation('auth');
const resetPasswordMutation = useRequestResetPassword();
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
const captcha = useCaptcha({ siteKey: params.captchaSiteKey });
const error = resetPasswordMutation.error;
const success = resetPasswordMutation.data;
@@ -67,10 +68,10 @@ export function PasswordResetRequestContainer(params: {
.mutateAsync({
email,
redirectTo,
captchaToken,
captchaToken: captcha.token,
})
.catch(() => {
resetCaptchaToken();
captcha.reset();
});
})}
className={'w-full'}
@@ -78,6 +79,8 @@ export function PasswordResetRequestContainer(params: {
<div className={'flex flex-col gap-y-4'}>
<AuthErrorAlert error={error} />
{captcha.field}
<FormField
name={'email'}
render={({ field }) => (

View File

@@ -6,7 +6,7 @@ import type { z } from 'zod';
import { useSignInWithEmailPassword } from '@kit/supabase/hooks/use-sign-in-with-email-password';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import type { PasswordSignInSchema } from '../schemas/password-sign-in.schema';
import { AuthErrorAlert } from './auth-error-alert';
@@ -14,10 +14,12 @@ import { PasswordSignInForm } from './password-sign-in-form';
export function PasswordSignInContainer({
onSignIn,
captchaSiteKey,
}: {
onSignIn?: (userId?: string) => unknown;
captchaSiteKey?: string;
}) {
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
const captcha = useCaptcha({ siteKey: captchaSiteKey });
const signInMutation = useSignInWithEmailPassword();
const { recordAuthMethod } = useLastAuthMethod();
const isLoading = signInMutation.isPending;
@@ -28,7 +30,7 @@ export function PasswordSignInContainer({
try {
const data = await signInMutation.mutateAsync({
...credentials,
options: { captchaToken },
options: { captchaToken: captcha.token },
});
// Record successful password sign-in
@@ -42,22 +44,18 @@ export function PasswordSignInContainer({
} catch {
// wrong credentials, do nothing
} finally {
resetCaptchaToken();
captcha.reset();
}
},
[
captchaToken,
onSignIn,
resetCaptchaToken,
signInMutation,
recordAuthMethod,
],
[captcha, onSignIn, signInMutation, recordAuthMethod],
);
return (
<>
<AuthErrorAlert error={signInMutation.error} />
{captcha.field}
<PasswordSignInForm
onSubmit={onSubmit}
loading={isLoading}

View File

@@ -6,7 +6,7 @@ import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow';
import { AuthErrorAlert } from './auth-error-alert';
import { PasswordSignUpForm } from './password-sign-up-form';
@@ -18,6 +18,7 @@ interface EmailPasswordSignUpContainerProps {
};
onSignUp?: (userId?: string) => unknown;
emailRedirectTo: string;
captchaSiteKey?: string;
}
export function EmailPasswordSignUpContainer({
@@ -25,8 +26,9 @@ export function EmailPasswordSignUpContainer({
onSignUp,
emailRedirectTo,
displayTermsCheckbox,
captchaSiteKey,
}: EmailPasswordSignUpContainerProps) {
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
const captcha = useCaptcha({ siteKey: captchaSiteKey });
const {
signUp: onSignupRequested,
@@ -36,8 +38,8 @@ export function EmailPasswordSignUpContainer({
} = usePasswordSignUpFlow({
emailRedirectTo,
onSignUp,
captchaToken,
resetCaptchaToken,
captchaToken: captcha.token,
resetCaptchaToken: captcha.reset,
});
return (
@@ -49,6 +51,8 @@ export function EmailPasswordSignUpContainer({
<If condition={!showVerifyEmailAlert}>
<AuthErrorAlert error={error} />
{captcha.field}
<PasswordSignUpForm
onSubmit={onSignupRequested}
loading={loading}

View File

@@ -18,10 +18,14 @@ import {
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { useCaptchaToken } from '../captcha/client';
import { useCaptcha } from '../captcha/client';
export function ResendAuthLinkForm(props: { redirectPath?: string }) {
const resendLink = useResendLink();
export function ResendAuthLinkForm(props: {
redirectPath?: string;
captchaSiteKey?: string;
}) {
const captcha = useCaptcha({ siteKey: props.captchaSiteKey });
const resendLink = useResendLink(captcha.token);
const form = useForm({
resolver: zodResolver(z.object({ email: z.string().email() })),
@@ -52,12 +56,20 @@ export function ResendAuthLinkForm(props: { redirectPath?: string }) {
<form
className={'flex flex-col space-y-2'}
onSubmit={form.handleSubmit((data) => {
return resendLink.mutate({
const promise = resendLink.mutateAsync({
email: data.email,
redirectPath: props.redirectPath,
});
promise.finally(() => {
captcha.reset();
});
return promise;
})}
>
{captcha.field}
<FormField
render={({ field }) => {
return (
@@ -83,9 +95,8 @@ export function ResendAuthLinkForm(props: { redirectPath?: string }) {
);
}
function useResendLink() {
function useResendLink(captchaToken: string) {
const supabase = useSupabase();
const { captchaToken } = useCaptchaToken();
const mutationFn = async (props: {
email: string;

View File

@@ -30,6 +30,8 @@ export function SignInMethodsContainer(props: {
otp: boolean;
oAuth: Provider[];
};
captchaSiteKey?: string;
}) {
const router = useRouter();
@@ -48,18 +50,25 @@ export function SignInMethodsContainer(props: {
<LastAuthMethodHint />
<If condition={props.providers.password}>
<PasswordSignInContainer onSignIn={onSignIn} />
<PasswordSignInContainer
onSignIn={onSignIn}
captchaSiteKey={props.captchaSiteKey}
/>
</If>
<If condition={props.providers.magicLink}>
<MagicLinkAuthContainer
redirectUrl={redirectUrl}
shouldCreateUser={false}
captchaSiteKey={props.captchaSiteKey}
/>
</If>
<If condition={props.providers.otp}>
<OtpSignInContainer shouldCreateUser={false} />
<OtpSignInContainer
shouldCreateUser={false}
captchaSiteKey={props.captchaSiteKey}
/>
</If>
<If condition={props.providers.oAuth.length}>

View File

@@ -27,6 +27,7 @@ export function SignUpMethodsContainer(props: {
};
displayTermsCheckbox?: boolean;
captchaSiteKey?: string;
}) {
const redirectUrl = getCallbackUrl(props);
const defaultValues = getDefaultValues();
@@ -41,11 +42,15 @@ export function SignUpMethodsContainer(props: {
emailRedirectTo={redirectUrl}
defaultValues={defaultValues}
displayTermsCheckbox={props.displayTermsCheckbox}
captchaSiteKey={props.captchaSiteKey}
/>
</If>
<If condition={props.providers.otp}>
<OtpSignInContainer shouldCreateUser={true} />
<OtpSignInContainer
shouldCreateUser={true}
captchaSiteKey={props.captchaSiteKey}
/>
</If>
<If condition={props.providers.magicLink}>
@@ -54,6 +59,7 @@ export function SignUpMethodsContainer(props: {
shouldCreateUser={true}
defaultValues={defaultValues}
displayTermsCheckbox={props.displayTermsCheckbox}
captchaSiteKey={props.captchaSiteKey}
/>
</If>