import { Form, useNavigation, useSearchParams } from "react-router"; import { useTranslation } from "react-i18next"; import { useMemo, useState } from "react"; import { useRootData } from "~/utils/data/useRootData"; import LoadingButton from "~/components/ui/buttons/LoadingButton"; import ExclamationTriangleIcon from "~/components/ui/icons/ExclamationTriangleIcon"; import Captcha from "~/components/captcha/Captcha"; import InputText from "~/components/ui/input/InputText"; export type PasswordPolicy = { minLength: number; requireUpper: boolean; requireLower: boolean; requireDigit: boolean; requireSpecial: boolean; }; interface Props { requireCaptcha?: boolean; isVerifyingEmail?: boolean; isSettingUpAccount?: boolean; data?: { company?: string; firstName?: string; lastName?: string; email?: string; slug?: string }; error: string | undefined; /** * Marketing-funnel Phase 2 (Phase 05 #79). When present, hidden form * inputs are emitted so the register action can route the submission * through `convertLeadToTenant` (atomic Tenant + Lead-status update + * TenantAcquisition + outbox event). Email is rendered read-only to * prevent lead-hijacking ("I use your token but with my email"). */ leadConversion?: { leadId: string; token: string; planId?: string | null }; passwordPolicy?: PasswordPolicy; } export const RegisterForm = ({ requireCaptcha = false, isVerifyingEmail = false, isSettingUpAccount = false, data = {}, error, leadConversion, passwordPolicy, }: Props) => { const { t } = useTranslation(); const { appConfiguration, csrf } = useRootData(); const navigation = useNavigation(); const [password, setPassword] = useState(""); const [searchParams] = useSearchParams(); const redirect = searchParams.get("redirect") ?? undefined; const showPasswordInput = !appConfiguration.auth.requireEmailVerification || isVerifyingEmail || isSettingUpAccount; const passwordChecks = useMemo(() => getPasswordChecks(password, passwordPolicy), [password, passwordPolicy]); return (
{appConfiguration.auth.authMethods.emailPassword.enabled && (
{leadConversion ? ( <> {leadConversion.planId ? ( ) : null} ) : null} {/* Tenant */} {appConfiguration.auth.requireOrganization && (
{/* */}
)} {/* Tenant: End */} {/* Personal Info */} {appConfiguration.auth.requireName && (
{/* */}
{/* */}
)}
{/* */}
{showPasswordInput && (
{/* */} {passwordPolicy ? (
    {passwordChecks.map((check) => { const label = t(check.label, check.args); const status = check.valid ? t("account.register.passwordRequirements.met") : t("account.register.passwordRequirements.notMet"); return (
  • {label}
  • ); })}
) : null}
)} {/* Personal Info: End */}
{t("account.register.prompts.register.title")}
{error && navigation.state === "idle" ? (
{error}
) : null}
)}

{t("account.register.bySigningUp")}{" "} {t("account.register.termsAndConditions")} {" "} {t("account.register.andOur")}{" "} {t("account.register.privacyPolicy")} .

); }; export function getPasswordChecks(password: string, policy?: PasswordPolicy) { if (!policy) { return []; } const checks: { key: string; valid: boolean; label: string; args: Record }[] = [ { key: "minLength", valid: password.length >= policy.minLength, label: "account.register.passwordRequirements.minLength", args: { 0: policy.minLength }, }, ]; if (policy.requireUpper) { checks.push({ key: "upper", valid: /[A-Z]/.test(password), label: "account.register.passwordRequirements.upper", args: {} }); } if (policy.requireLower) { checks.push({ key: "lower", valid: /[a-z]/.test(password), label: "account.register.passwordRequirements.lower", args: {} }); } if (policy.requireDigit) { checks.push({ key: "digit", valid: /\d/.test(password), label: "account.register.passwordRequirements.digit", args: {} }); } if (policy.requireSpecial) { checks.push({ key: "special", valid: /[^A-Za-z0-9]/.test(password), label: "account.register.passwordRequirements.special", args: {} }); } return checks; }