update signup page
Build & Push / Pipeline Tests (push) Successful in 1m31s
Test / Type Check (all packages) (push) Successful in 52s
Build & Push / Build & Push Docker Image (push) Successful in 4m3s
Test / API Unit Tests (push) Successful in 1m2s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Carplace Unit Tests (push) Successful in 41s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 42s
Test / API Integration Tests (push) Successful in 1m2s

This commit is contained in:
root
2026-07-20 23:21:24 -04:00
parent 93fba7a318
commit 7ecd85e9b7
4 changed files with 127 additions and 10 deletions
@@ -5,6 +5,9 @@ import { z } from 'zod'
* See progressive-signup-plan.md Section 3.
*/
export const accountStartSchema = z.object({
firstName: z.string().trim().min(1).max(80),
lastName: z.string().trim().min(1).max(80),
companyName: z.string().trim().min(2).max(120),
email: z.string().email(),
password: z.string().min(8).max(128),
preferredLanguage: z.enum(['en', 'fr', 'ar']).default('en'),
@@ -42,7 +42,7 @@ export async function startAccount(body: AccountStartInput) {
const company = await tx.company.create({
data: {
name: '',
name: body.companyName,
slug,
email: body.email,
address: {},
@@ -53,7 +53,7 @@ export async function startAccount(body: AccountStartInput) {
await tx.brandSettings.create({
data: {
companyId: company.id,
displayName: body.email.split('@')[0] || 'My Workspace',
displayName: body.companyName,
subdomain: slug,
defaultLocale: body.preferredLanguage,
defaultCurrency: 'MAD',
@@ -78,8 +78,8 @@ export async function startAccount(body: AccountStartInput) {
data: {
companyId: company.id,
clerkUserId: `local_owner_${company.id}`,
firstName: '',
lastName: '',
firstName: body.firstName,
lastName: body.lastName,
email: body.email,
passwordHash,
emailVerificationToken: hashPublicAccessToken(verificationToken),
@@ -128,6 +128,9 @@ describe('auth API boundaries', () => {
it('POST /auth/account/start validates minimal signup and returns 201', async () => {
const res = await request(app).post('/api/v1/auth/account/start').send({
firstName: 'Aya',
lastName: 'Benali',
companyName: 'Atlas Cars',
email: 'owner@example.com',
password: 'super-secret',
preferredLanguage: 'fr',
@@ -141,6 +144,9 @@ describe('auth API boundaries', () => {
},
})
expect(accountService.startAccount).toHaveBeenCalledWith({
firstName: 'Aya',
lastName: 'Benali',
companyName: 'Atlas Cars',
email: 'owner@example.com',
password: 'super-secret',
preferredLanguage: 'fr',
@@ -149,6 +155,9 @@ describe('auth API boundaries', () => {
it('POST /auth/account/start rejects malformed minimal signup payloads before service execution', async () => {
const res = await request(app).post('/api/v1/auth/account/start').send({
firstName: 'Aya',
lastName: 'Benali',
companyName: 'A',
email: 'not-email',
password: 'short',
})
@@ -48,8 +48,12 @@ export default function SignUpForm({
? requestedLanguage
: language;
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [companyName, setCompanyName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [preferredLanguage, setPreferredLanguage] = useState<
"en" | "fr" | "ar"
@@ -131,11 +135,18 @@ export default function SignUpForm({
const dict = {
en: {
title: "Create account",
subtitle: "Enter your email and password to get started.",
subtitle: "Enter your details to create your workspace.",
firstName: "First name",
firstNamePlaceholder: "Aya",
lastName: "Last name",
lastNamePlaceholder: "Benali",
company: "Company",
companyPlaceholder: "Atlas Cars",
email: "Email",
emailPlaceholder: "you@company.com",
password: "Password",
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
confirmPassword: "Confirm password",
passwordHint: "Minimum 8 characters",
languageLabel: "Preferred language",
create: "Create account",
@@ -143,6 +154,7 @@ export default function SignUpForm({
alreadyHave: "Already have an account?",
signIn: "Sign in",
errorEmailTaken: "An account with this email already exists.",
errorPasswordMismatch: "Passwords do not match.",
errorGeneric: "Something went wrong. Please try again.",
successTitle: "Check your email",
successMessage:
@@ -151,11 +163,18 @@ export default function SignUpForm({
},
fr: {
title: "Cr\u00e9er un compte",
subtitle: "Saisissez votre email et mot de passe pour commencer.",
subtitle: "Saisissez vos informations pour cr\u00e9er votre espace.",
firstName: "Pr\u00e9nom",
firstNamePlaceholder: "Aya",
lastName: "Nom",
lastNamePlaceholder: "Benali",
company: "Entreprise",
companyPlaceholder: "Atlas Cars",
email: "Email",
emailPlaceholder: "vous@entreprise.com",
password: "Mot de passe",
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
confirmPassword: "Confirmer le mot de passe",
passwordHint: "Minimum 8 caract\u00e8res",
languageLabel: "Langue pr\u00e9f\u00e9r\u00e9e",
create: "Cr\u00e9er un compte",
@@ -163,6 +182,7 @@ export default function SignUpForm({
alreadyHave: "Vous avez d\u00e9j\u00e0 un compte ?",
signIn: "Se connecter",
errorEmailTaken: "Un compte avec cet email existe d\u00e9j\u00e0.",
errorPasswordMismatch: "Les mots de passe ne correspondent pas.",
errorGeneric: "Une erreur est survenue. Veuillez r\u00e9essayer.",
successTitle: "V\u00e9rifiez votre email",
successMessage:
@@ -173,12 +193,20 @@ export default function SignUpForm({
title:
"\u0623\u0646\u0634\u0626 \u062d\u0633\u0627\u0628\u064b\u0627",
subtitle:
"\u0623\u062f\u062e\u0644 \u0628\u0631\u064a\u062f\u0643 \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0648\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631 \u0644\u0644\u0628\u062f\u0621.",
"\u0623\u062f\u062e\u0644 \u0628\u064a\u0627\u0646\u0627\u062a\u0643 \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0633\u0627\u062d\u0629 \u0639\u0645\u0644\u0643.",
firstName: "\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0623\u0648\u0644",
firstNamePlaceholder: "Aya",
lastName: "\u0627\u0633\u0645 \u0627\u0644\u0639\u0627\u0626\u0644\u0629",
lastNamePlaceholder: "Benali",
company: "\u0627\u0644\u0634\u0631\u0643\u0629",
companyPlaceholder: "Atlas Cars",
email:
"\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a",
emailPlaceholder: "you@company.com",
password: "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631",
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
confirmPassword:
"\u062a\u0623\u0643\u064a\u062f \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631",
passwordHint:
"\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 8 \u0623\u062d\u0631\u0641",
languageLabel:
@@ -193,6 +221,8 @@ export default function SignUpForm({
"\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644",
errorEmailTaken:
"\u064a\u0648\u062c\u062f \u062d\u0633\u0627\u0628 \u0628\u0647\u0630\u0627 \u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0628\u0627\u0644\u0641\u0639\u0644.",
errorPasswordMismatch:
"\u0643\u0644\u0645\u062a\u0627 \u0627\u0644\u0645\u0631\u0648\u0631 \u063a\u064a\u0631 \u0645\u062a\u0637\u0627\u0628\u0642\u062a\u064a\u0646.",
errorGeneric:
"\u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627. \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.",
successTitle:
@@ -212,13 +242,25 @@ export default function SignUpForm({
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (password !== confirmPassword) {
setError(dict.errorPasswordMismatch);
return;
}
setLoading(true);
setError(null);
try {
await apiFetch("/auth/account/start", {
method: "POST",
body: JSON.stringify({ email, password, preferredLanguage }),
body: JSON.stringify({
firstName,
lastName,
companyName,
email,
password,
preferredLanguage,
}),
});
setLanguage(preferredLanguage);
@@ -277,7 +319,7 @@ export default function SignUpForm({
return (
<PublicShell embedded={embedded} hideFooter hideHeaderActions>
<main className="flex flex-1 items-center justify-center px-4 py-12">
<div className="w-full max-w-md">
<div className="w-full max-w-lg">
<div className="mb-8 text-center">
<div className="flex justify-center">
<a href={websiteUrl} target="_top">
@@ -307,6 +349,55 @@ export default function SignUpForm({
) : null}
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
{dict.firstName} <span className="text-red-500">*</span>
</label>
<input
type="text"
required
maxLength={80}
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder={dict.firstNamePlaceholder}
autoFocus
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
{dict.lastName} <span className="text-red-500">*</span>
</label>
<input
type="text"
required
maxLength={80}
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder={dict.lastNamePlaceholder}
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
{dict.company} <span className="text-red-500">*</span>
</label>
<input
type="text"
required
minLength={2}
maxLength={120}
value={companyName}
onChange={(e) => setCompanyName(e.target.value)}
placeholder={dict.companyPlaceholder}
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
{dict.email} <span className="text-red-500">*</span>
@@ -317,7 +408,6 @@ export default function SignUpForm({
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={dict.emailPlaceholder}
autoFocus
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>
@@ -387,6 +477,21 @@ export default function SignUpForm({
</p>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
{dict.confirmPassword} <span className="text-red-500">*</span>
</label>
<input
type={showPassword ? "text" : "password"}
required
minLength={8}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder={dict.passwordPlaceholder}
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>
<button
type="submit"
disabled={loading}