import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' import { motion } from 'framer-motion' import { CheckCircle, Eye, EyeOff } from 'lucide-react' import { useRouter } from 'next/router' import { parseAsString, useQueryStates } from 'nuqs' import { useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Alert, AlertDescription, AlertTitle, Button, cn, Form, FormControl, FormField, Input, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import PasswordConditionsHelper from './PasswordConditionsHelper' import { useSignUpMutation } from '@/data/misc/signup-mutation' import { BASE_PATH } from '@/lib/constants' import { buildPathWithParams } from '@/lib/gotrue' const schema = z.object({ email: z.string().min(1, 'Email is required').email('Must be a valid email'), password: z .string() .min(1, 'Password is required') .max(72, 'Password cannot exceed 72 characters') .refine((password) => password.length >= 8, 'Password must be at least 8 characters') .refine( (password) => /[A-Z]/.test(password), 'Password must contain at least 1 uppercase character' ) .refine( (password) => /[a-z]/.test(password), 'Password must contain at least 1 lowercase character' ) .refine((password) => /[0-9]/.test(password), 'Password must contain at least 1 number') .refine( (password) => /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password), 'Password must contain at least 1 symbol' ), }) const formId = 'sign-up-form' export const SignUpForm = () => { const captchaRef = useRef(null) const [showConditions, setShowConditions] = useState(false) const [isSubmitted, setIsSubmitted] = useState(false) const [passwordHidden, setPasswordHidden] = useState(true) const [captchaToken, setCaptchaToken] = useState(null) const router = useRouter() const form = useForm>({ resolver: zodResolver(schema as any), defaultValues: { email: '', password: '' }, }) const [searchParams] = useQueryStates({ auth_id: parseAsString.withDefault(''), token: parseAsString.withDefault(''), }) const { mutate: signup, isPending: isSigningUp } = useSignUpMutation({ onSuccess: () => { toast.success(`Signed up successfully!`) setIsSubmitted(true) }, onError: (error) => { setCaptchaToken(null) captchaRef.current?.resetCaptcha() toast.error(`Failed to sign up: ${error.message}`) }, }) const onSubmit: SubmitHandler> = async ({ email, password }) => { // [Joshen] Separate submitting state as there's 2 async processes here let token = captchaToken if (!token) { const captchaResponse = await captchaRef.current?.execute({ async: true }) token = captchaResponse?.response ?? null } const isInsideOAuthFlow = !!searchParams.auth_id const redirectUrlBase = `${ process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ? location.origin : process.env.NEXT_PUBLIC_SITE_URL }${BASE_PATH}` let redirectTo: string if (isInsideOAuthFlow) { redirectTo = `${redirectUrlBase}/authorize?auth_id=${searchParams.auth_id}${searchParams.token && `&token=${searchParams.token}`}` } else { // Use getRedirectToPath to handle redirect_to parameter and other query params const { returnTo } = router.query const basePath = returnTo || '/sign-in' const fullPath = buildPathWithParams(basePath as string) const fullRedirectUrl = `${redirectUrlBase}${fullPath}` redirectTo = fullRedirectUrl } signup({ email, password, hcaptchaToken: token ?? null, redirectTo, }) } const password = form.watch('password') const isSubmitting = form.formState.isSubmitting || isSigningUp return (
{isSubmitted && ( Check your email to confirm You've successfully signed up. Please check your email to confirm your account before signing in to the Briven dashboard. The confirmation link expires in 10 minutes. )}
( )} /> (
setShowConditions(true)} disabled={isSubmitting} />
)} />
setCaptchaToken(token)} onExpire={() => setCaptchaToken(null)} />
) }