"use client"; import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Progress } from "@/components/ui/progress"; import { SignUpResponse } from "@server/routers/auth"; import { useRouter } from "next/navigation"; import { passwordSchema } from "@server/auth/passwordSchema"; import { AxiosResponse } from "axios"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; import { build } from "@server/build"; import { Check, X } from "lucide-react"; import { cn } from "@app/lib/cn"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; // Password strength calculation const calculatePasswordStrength = (password: string) => { const requirements = { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), number: /[0-9]/.test(password), special: /[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]/.test(password) }; const score = Object.values(requirements).filter(Boolean).length; let strength: "weak" | "medium" | "strong" = "weak"; let color = "bg-red-500"; let percentage = 0; if (score >= 5) { strength = "strong"; color = "bg-green-500"; percentage = 100; } else if (score >= 3) { strength = "medium"; color = "bg-yellow-500"; percentage = 60; } else if (score >= 1) { strength = "weak"; color = "bg-red-500"; percentage = 30; } return { requirements, strength, color, percentage, score }; }; type SignupFormProps = { redirect?: string; inviteId?: string; inviteToken?: string; emailParam?: string; }; const formSchema = z .object({ email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, confirmPassword: passwordSchema, agreeToTerms: z.boolean().refine( (val) => { if (build === "saas") { val === true; } return true; }, { message: "You must agree to the terms of service and privacy policy" } ), marketingEmailConsent: z.boolean().optional() }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], message: "Passwords do not match" }); export default function SignupForm({ redirect, inviteId, inviteToken, emailParam }: SignupFormProps) { const router = useRouter(); const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); const { isUnlocked } = useLicenseStatusContext(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [termsAgreedAt, setTermsAgreedAt] = useState(null); const [passwordValue, setPasswordValue] = useState(""); const [confirmPasswordValue, setConfirmPasswordValue] = useState(""); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { email: emailParam || "", password: "", confirmPassword: "", agreeToTerms: false, marketingEmailConsent: false }, mode: "onChange" // Enable real-time validation }); const passwordStrength = calculatePasswordStrength(passwordValue); const doPasswordsMatch = passwordValue.length > 0 && confirmPasswordValue.length > 0 && passwordValue === confirmPasswordValue; async function onSubmit(values: z.infer) { const { email, password, marketingEmailConsent } = values; setLoading(true); const res = await api .put>("/auth/signup", { email, password, inviteId, inviteToken, termsAcceptedTimestamp: termsAgreedAt, marketingEmailConsent: build === "saas" ? marketingEmailConsent : undefined }) .catch((e) => { console.error(e); setError(formatAxiosError(e, t("signupError"))); }); if (res && res.status === 200) { setError(null); if (res.data?.data?.emailVerificationRequired) { if (redirect) { const safe = cleanRedirect(redirect); router.push(`/auth/verify-email?redirect=${safe}`); } else { router.push("/auth/verify-email"); } return; } if (redirect) { const safe = cleanRedirect(redirect); router.push(safe); } else { router.push("/"); } } setLoading(false); } function getSubtitle() { if (isUnlocked() && env.branding?.signupPage?.subtitleText) { return env.branding.signupPage.subtitleText; } return t("authCreateAccount"); } const handleTermsChange = (checked: boolean) => { if (checked) { const isoNow = new Date().toISOString(); console.log("Terms agreed at:", isoNow); setTermsAgreedAt(isoNow); form.setValue("agreeToTerms", true); } else { form.setValue("agreeToTerms", false); setTermsAgreedAt(null); } }; const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 175 : 175; const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 58 : 58; return (

{getSubtitle()}

( {t("email")} )} /> (
{t("password")} {passwordStrength.strength === "strong" && ( )}
{ field.onChange(e); setPasswordValue( e.target.value ); }} className={cn( passwordStrength.strength === "strong" && "border-green-500 focus-visible:ring-green-500", passwordStrength.strength === "medium" && "border-yellow-500 focus-visible:ring-yellow-500", passwordStrength.strength === "weak" && passwordValue.length > 0 && "border-red-500 focus-visible:ring-red-500" )} autoComplete="new-password" />
{passwordValue.length > 0 && (
{/* Password Strength Meter */}
{t("passwordStrength")} {t( `passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}` )}
{/* Requirements Checklist */}
{t("passwordRequirements")}
{passwordStrength .requirements .length ? ( ) : ( )} {t( "passwordRequirementLengthText" )}
{passwordStrength .requirements .uppercase ? ( ) : ( )} {t( "passwordRequirementUppercaseText" )}
{passwordStrength .requirements .lowercase ? ( ) : ( )} {t( "passwordRequirementLowercaseText" )}
{passwordStrength .requirements .number ? ( ) : ( )} {t( "passwordRequirementNumberText" )}
{passwordStrength .requirements .special ? ( ) : ( )} {t( "passwordRequirementSpecialText" )}
)} {/* Only show FormMessage when not showing our custom requirements */} {passwordValue.length === 0 && ( )}
)} /> (
{t("confirmPassword")} {doPasswordsMatch && ( )}
{ field.onChange(e); setConfirmPasswordValue( e.target.value ); }} className={cn( doPasswordsMatch && "border-green-500 focus-visible:ring-green-500", confirmPasswordValue.length > 0 && !doPasswordsMatch && "border-red-500 focus-visible:ring-red-500" )} autoComplete="new-password" />
{confirmPasswordValue.length > 0 && !doPasswordsMatch && (

{t("passwordsDoNotMatch")}

)} {/* Only show FormMessage when field is empty */} {confirmPasswordValue.length === 0 && ( )}
)} /> {build === "saas" && ( <> ( { field.onChange(checked); handleTermsChange( checked as boolean ); }} />
{t( "signUpTerms.IAgreeToThe" )}{" "} {t( "signUpTerms.termsOfService" )}{" "} {t("signUpTerms.and")}{" "} {t( "signUpTerms.privacyPolicy" )}
)} /> (
{t( "signUpMarketing.keepMeInTheLoop" )}
)} /> )} {error && ( {error} )}
); }