Captcha Refactoring (#397)
* refactor: replace useCaptchaToken with useCaptcha hook and integrate CaptchaField across forms
This commit is contained in:
committed by
GitHub
parent
9eccb319af
commit
ea0c1dde80
@@ -46,7 +46,11 @@ async function SignInPage({ searchParams }: SignInPageProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SignInMethodsContainer paths={paths} providers={authConfig.providers} />
|
||||
<SignInMethodsContainer
|
||||
paths={paths}
|
||||
providers={authConfig.providers}
|
||||
captchaSiteKey={authConfig.captchaTokenSiteKey}
|
||||
/>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<Button asChild variant={'link'} size={'sm'}>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
|
||||
import { CaptchaProvider } from '@kit/auth/captcha/client';
|
||||
import { I18nProvider } from '@kit/i18n/provider';
|
||||
import { MonitoringProvider } from '@kit/monitoring/components';
|
||||
import { AppEventsProvider } from '@kit/shared/events';
|
||||
@@ -16,27 +13,12 @@ import { VersionUpdater } from '@kit/ui/version-updater';
|
||||
import { AnalyticsProvider } from '~/components/analytics-provider';
|
||||
import { AuthProvider } from '~/components/auth-provider';
|
||||
import appConfig from '~/config/app.config';
|
||||
import authConfig from '~/config/auth.config';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
|
||||
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
|
||||
|
||||
import { ReactQueryProvider } from './react-query-provider';
|
||||
|
||||
const captchaSiteKey = authConfig.captchaTokenSiteKey;
|
||||
|
||||
const CaptchaTokenSetter = dynamic(async () => {
|
||||
if (!captchaSiteKey) {
|
||||
return Promise.resolve(() => null);
|
||||
}
|
||||
|
||||
const { CaptchaTokenSetter } = await import('@kit/auth/captcha/client');
|
||||
|
||||
return {
|
||||
default: CaptchaTokenSetter,
|
||||
};
|
||||
});
|
||||
|
||||
type RootProvidersProps = React.PropsWithChildren<{
|
||||
// The language to use for the app (optional)
|
||||
lang?: string;
|
||||
@@ -60,9 +42,6 @@ export function RootProviders({
|
||||
<AnalyticsProvider>
|
||||
<ReactQueryProvider>
|
||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} nonce={nonce} />
|
||||
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
@@ -75,7 +54,6 @@ export function RootProviders({
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</CaptchaProvider>
|
||||
|
||||
<If condition={featuresFlagConfig.enableVersionUpdater}>
|
||||
<VersionUpdater />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-supabase-saas-kit-turbo",
|
||||
"version": "2.18.2",
|
||||
"version": "2.18.3",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
|
||||
125
packages/features/auth/src/captcha/client/captcha-field.tsx
Normal file
125
packages/features/auth/src/captcha/client/captcha-field.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import {
|
||||
Turnstile,
|
||||
TurnstileInstance,
|
||||
TurnstileProps,
|
||||
} from '@marsidev/react-turnstile';
|
||||
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
||||
import { useController } from 'react-hook-form';
|
||||
|
||||
interface BaseCaptchaFieldProps {
|
||||
siteKey: string | undefined;
|
||||
options?: TurnstileProps;
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
interface StandaloneCaptchaFieldProps extends BaseCaptchaFieldProps {
|
||||
onTokenChange: (token: string) => void;
|
||||
onInstanceChange?: (instance: TurnstileInstance | null) => void;
|
||||
control?: never;
|
||||
name?: never;
|
||||
}
|
||||
|
||||
interface ReactHookFormCaptchaFieldProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> extends BaseCaptchaFieldProps {
|
||||
control: Control<TFieldValues>;
|
||||
name: TName;
|
||||
onTokenChange?: never;
|
||||
onInstanceChange?: never;
|
||||
}
|
||||
|
||||
type CaptchaFieldProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> =
|
||||
| StandaloneCaptchaFieldProps
|
||||
| ReactHookFormCaptchaFieldProps<TFieldValues, TName>;
|
||||
|
||||
/**
|
||||
* @name CaptchaField
|
||||
* @description Self-contained captcha component with two modes:
|
||||
*
|
||||
* **Standalone mode** - For use outside react-hook-form:
|
||||
* ```tsx
|
||||
* <CaptchaField
|
||||
* siteKey={siteKey}
|
||||
* onTokenChange={setToken}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* **React Hook Form mode** - Automatic form integration:
|
||||
* ```tsx
|
||||
* <CaptchaField
|
||||
* siteKey={siteKey}
|
||||
* control={form.control}
|
||||
* name="captchaToken"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function CaptchaField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(props: CaptchaFieldProps<TFieldValues, TName>) {
|
||||
const { siteKey, options, nonce } = props;
|
||||
const instanceRef = useRef<TurnstileInstance | null>(null);
|
||||
|
||||
// React Hook Form integration
|
||||
const controller =
|
||||
'control' in props && props.control
|
||||
? // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useController({
|
||||
control: props.control,
|
||||
name: props.name,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!siteKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultOptions: Partial<TurnstileProps> = {
|
||||
options: {
|
||||
size: 'invisible',
|
||||
},
|
||||
};
|
||||
|
||||
const handleSuccess = (token: string) => {
|
||||
if (controller) {
|
||||
// React Hook Form mode - use setValue from controller
|
||||
controller.field.onChange(token);
|
||||
} else if ('onTokenChange' in props && props.onTokenChange) {
|
||||
// Standalone mode
|
||||
props.onTokenChange(token);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstanceChange = (instance: TurnstileInstance | null) => {
|
||||
instanceRef.current = instance;
|
||||
|
||||
if ('onInstanceChange' in props && props.onInstanceChange) {
|
||||
props.onInstanceChange(instance);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Turnstile
|
||||
ref={(instance) => {
|
||||
if (instance) {
|
||||
handleInstanceChange(instance);
|
||||
}
|
||||
}}
|
||||
siteKey={siteKey}
|
||||
onSuccess={handleSuccess}
|
||||
scriptOptions={{
|
||||
nonce,
|
||||
}}
|
||||
{...defaultOptions}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { TurnstileInstance } from '@marsidev/react-turnstile';
|
||||
|
||||
export const Captcha = createContext<{
|
||||
token: string;
|
||||
setToken: (token: string) => void;
|
||||
instance: TurnstileInstance | null;
|
||||
setInstance: (ref: TurnstileInstance) => void;
|
||||
}>({
|
||||
token: '',
|
||||
instance: null,
|
||||
setToken: (_: string) => {
|
||||
// do nothing
|
||||
return '';
|
||||
},
|
||||
setInstance: () => {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
|
||||
export function CaptchaProvider(props: { children: React.ReactNode }) {
|
||||
const [token, setToken] = useState<string>('');
|
||||
const instanceRef = useRef<TurnstileInstance | null>(null);
|
||||
|
||||
const setInstance = useCallback((ref: TurnstileInstance) => {
|
||||
instanceRef.current = ref;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Captcha.Provider
|
||||
value={{ token, setToken, instance: instanceRef.current, setInstance }}
|
||||
>
|
||||
{props.children}
|
||||
</Captcha.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { Turnstile, TurnstileProps } from '@marsidev/react-turnstile';
|
||||
|
||||
import { Captcha } from './captcha-provider';
|
||||
|
||||
export function CaptchaTokenSetter(props: {
|
||||
siteKey: string | undefined;
|
||||
options?: TurnstileProps;
|
||||
nonce?: string;
|
||||
}) {
|
||||
const { setToken, setInstance } = useContext(Captcha);
|
||||
|
||||
if (!props.siteKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = props.options ?? {
|
||||
options: {
|
||||
size: 'invisible',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Turnstile
|
||||
ref={(instance) => {
|
||||
if (instance) {
|
||||
setInstance(instance);
|
||||
}
|
||||
}}
|
||||
siteKey={props.siteKey}
|
||||
onSuccess={setToken}
|
||||
scriptOptions={{
|
||||
nonce: props.nonce,
|
||||
}}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './captcha-token-setter';
|
||||
export * from './use-captcha-token';
|
||||
export * from './captcha-provider';
|
||||
export * from './captcha-field';
|
||||
export * from './use-captcha';
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
import { Captcha } from './captcha-provider';
|
||||
|
||||
/**
|
||||
* @name useCaptchaToken
|
||||
* @description A hook to get the captcha token and reset function
|
||||
* @returns The captcha token and reset function
|
||||
*/
|
||||
export function useCaptchaToken() {
|
||||
const context = useContext(Captcha);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useCaptchaToken must be used within a CaptchaProvider`);
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
captchaToken: context.token,
|
||||
resetCaptchaToken: () => context.instance?.reset(),
|
||||
};
|
||||
}, [context]);
|
||||
}
|
||||
81
packages/features/auth/src/captcha/client/use-captcha.tsx
Normal file
81
packages/features/auth/src/captcha/client/use-captcha.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { TurnstileInstance } from '@marsidev/react-turnstile';
|
||||
|
||||
import { CaptchaField } from './captcha-field';
|
||||
|
||||
/**
|
||||
* @name useCaptcha
|
||||
* @description Zero-boilerplate hook for captcha integration.
|
||||
* Manages token state and instance internally, exposing a clean API.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function SignInForm({ captchaSiteKey }) {
|
||||
* const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||
*
|
||||
* const handleSubmit = async (data) => {
|
||||
* await signIn({ ...data, captchaToken: captcha.token });
|
||||
* captcha.reset();
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <form onSubmit={handleSubmit}>
|
||||
* {captcha.field}
|
||||
* <button>Submit</button>
|
||||
* </form>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useCaptcha(
|
||||
{ siteKey, nonce }: { siteKey?: string; nonce?: string } = {
|
||||
siteKey: undefined,
|
||||
nonce: undefined,
|
||||
},
|
||||
) {
|
||||
const [token, setToken] = useState('');
|
||||
const instanceRef = useRef<TurnstileInstance | null>(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(
|
||||
() => (
|
||||
<CaptchaField
|
||||
siteKey={siteKey}
|
||||
onTokenChange={handleTokenChange}
|
||||
onInstanceChange={handleInstanceChange}
|
||||
nonce={nonce}
|
||||
/>
|
||||
),
|
||||
[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],
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user