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
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user