diff --git a/apps/web/app/auth/sign-in/page.tsx b/apps/web/app/auth/sign-in/page.tsx index 0205f68e2..522738810 100644 --- a/apps/web/app/auth/sign-in/page.tsx +++ b/apps/web/app/auth/sign-in/page.tsx @@ -46,7 +46,11 @@ async function SignInPage({ searchParams }: SignInPageProps) {

- +
+ * + * ); + * } + * ``` + */ +export function useCaptcha( + { siteKey, nonce }: { siteKey?: string; nonce?: string } = { + siteKey: undefined, + nonce: undefined, + }, +) { + const [token, setToken] = useState(''); + const instanceRef = useRef(null); + + const reset = useCallback(() => { + instanceRef.current?.reset(); + setToken(''); + }, []); + + const handleTokenChange = useCallback((newToken: string) => { + setToken(newToken); + }, []); + + const handleInstanceChange = useCallback( + (instance: TurnstileInstance | null) => { + instanceRef.current = instance; + }, + [], + ); + + const field = useMemo( + () => ( + + ), + [siteKey, nonce, handleTokenChange, handleInstanceChange], + ); + + return useMemo( + () => ({ + /** The current captcha token */ + token, + /** Reset the captcha (clears token and resets widget) */ + reset, + /** The captcha field component to render */ + field, + }), + [token, reset, field], + ); +} diff --git a/packages/features/auth/src/components/magic-link-auth-container.tsx b/packages/features/auth/src/components/magic-link-auth-container.tsx index 4fe8653e9..d8f81eef3 100644 --- a/packages/features/auth/src/components/magic-link-auth-container.tsx +++ b/packages/features/auth/src/components/magic-link-auth-container.tsx @@ -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({ + {captcha.field} + ( diff --git a/packages/features/auth/src/components/otp-sign-in-container.tsx b/packages/features/auth/src/components/otp-sign-in-container.tsx index 1c228c891..3403cfed7 100644 --- a/packages/features/auth/src/components/otp-sign-in-container.tsx +++ b/packages/features/auth/src/components/otp-sign-in-container.tsx @@ -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 ( { 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) => { await signInMutation.mutateAsync({ email, - options: { captchaToken, shouldCreateUser }, + options: { captchaToken: captcha.token, shouldCreateUser }, }); - resetCaptchaToken(); + captcha.reset(); onSendOtp(email); }; @@ -205,6 +209,8 @@ function OtpEmailForm({ > + {captcha.field} + ( diff --git a/packages/features/auth/src/components/password-reset-request-container.tsx b/packages/features/auth/src/components/password-reset-request-container.tsx index da86376f1..6065c40de 100644 --- a/packages/features/auth/src/components/password-reset-request-container.tsx +++ b/packages/features/auth/src/components/password-reset-request-container.tsx @@ -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: {
+ {captcha.field} + ( diff --git a/packages/features/auth/src/components/password-sign-in-container.tsx b/packages/features/auth/src/components/password-sign-in-container.tsx index 450c29bac..4bf095ce8 100644 --- a/packages/features/auth/src/components/password-sign-in-container.tsx +++ b/packages/features/auth/src/components/password-sign-in-container.tsx @@ -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 ( <> + {captcha.field} + 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({ + {captcha.field} + { - return resendLink.mutate({ + const promise = resendLink.mutateAsync({ email: data.email, redirectPath: props.redirectPath, }); + + promise.finally(() => { + captcha.reset(); + }); + + return promise; })} > + {captcha.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; diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index bdc031f01..7ed64241b 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -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: { - + - + diff --git a/packages/features/auth/src/components/sign-up-methods-container.tsx b/packages/features/auth/src/components/sign-up-methods-container.tsx index 780c2da29..266d20f88 100644 --- a/packages/features/auth/src/components/sign-up-methods-container.tsx +++ b/packages/features/auth/src/components/sign-up-methods-container.tsx @@ -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} /> - + @@ -54,6 +59,7 @@ export function SignUpMethodsContainer(props: { shouldCreateUser={true} defaultValues={defaultValues} displayTermsCheckbox={props.displayTermsCheckbox} + captchaSiteKey={props.captchaSiteKey} />