From 662b987d931034a19400ac5272c3b6a07758e0fa Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 1 Jun 2026 18:27:38 +0800 Subject: [PATCH 1/2] feat(v2): v2-native sign-up (V2Register) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /v2/register rendered the legacy MUI Register form — inconsistent with the v2 login + landing now that v2 is the default. Add V2Register, reusing the .v2-login styles (+ a username field), honoring the invite-only policy and surfacing the backend's register message with a sign-in handoff. Route /v2/register → V2Register. Update register e2e to the v2 selectors ("Create account", v2 success state). Co-Authored-By: Claude Opus 4.8 --- e2e/auth.spec.ts | 9 +- frontend/src/v2/V2App.tsx | 4 +- frontend/src/v2/components/V2Register.tsx | 148 ++++++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 frontend/src/v2/components/V2Register.tsx diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index e57fa7d1..2be48238 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -8,7 +8,7 @@ test.describe('Authentication', () => { await expect(page.getByLabel('Username')).toBeVisible(); await expect(page.getByLabel('Email')).toBeVisible(); await expect(page.getByLabel('Password')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Register' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Create account' })).toBeVisible(); }); test('register new user shows success message', async ({ page }) => { @@ -17,11 +17,10 @@ test.describe('Authentication', () => { await page.getByLabel('Username').fill(`user_${tag}`); await page.getByLabel('Email').fill(`${tag}@commonly.test`); await page.getByLabel('Password').fill('TestPass123!'); - await page.getByRole('button', { name: 'Register' }).click(); + await page.getByRole('button', { name: 'Create account' }).click(); - // Success message from res.data.message — exact text depends on backend - // but it must be non-error text visible on the page - await expect(page.locator('.MuiTypography-root').filter({ hasNotText: /Register|Create|Start/ }).last()).toBeVisible({ timeout: 8000 }); + // v2 register swaps to a success state with a "Continue to sign in" CTA + await expect(page.getByRole('button', { name: 'Continue to sign in' })).toBeVisible({ timeout: 8000 }); }); test('login with wrong password shows error', async ({ page }) => { diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index 83ab91fd..3e19383f 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -6,7 +6,7 @@ import V2FeaturePage from './components/V2FeaturePage'; import V2YourTeamPage from './components/V2YourTeamPage'; import V2InviteRedeem from './components/V2InviteRedeem'; import { useAuth } from '../context/AuthContext'; -import Register from '../components/Register'; +import V2Register from './components/V2Register'; import RegistrationInviteRequired from '../components/RegistrationInviteRequired'; import VerifyEmail from '../components/VerifyEmail'; import DiscordCallback from '../components/DiscordCallback'; @@ -125,7 +125,7 @@ const V2App: React.FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/v2/components/V2Register.tsx b/frontend/src/v2/components/V2Register.tsx new file mode 100644 index 00000000..9541d6db --- /dev/null +++ b/frontend/src/v2/components/V2Register.tsx @@ -0,0 +1,148 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams, Navigate, Link } from 'react-router-dom'; +import axios from '../../utils/axiosConfig'; + +// v2-native sign-up. Pairs with V2Login (reuses the .v2-login styles) so the +// auth surfaces match after v2 became the default. Mirrors the legacy +// Register flow: honor the invite-only policy, POST /api/auth/register, then +// surface the backend's message and hand off to sign-in (the backend may send +// a verification email; it does not always return a usable session). + +interface RegistrationPolicy { + loaded: boolean; + inviteOnly: boolean; +} + +const V2Register: React.FC = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [invitationCode] = useState(searchParams.get('invite') || ''); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [done, setDone] = useState(null); + const [policy, setPolicy] = useState({ loaded: false, inviteOnly: false }); + + useEffect(() => { + let active = true; + axios.get('/api/auth/registration-policy') + .then((res) => { if (active) setPolicy({ loaded: true, inviteOnly: Boolean(res.data?.inviteOnly) }); }) + .catch(() => { if (active) setPolicy({ loaded: true, inviteOnly: false }); }); + return () => { active = false; }; + }, []); + + const hasInviteFromUrl = useMemo(() => Boolean(searchParams.get('invite')), [searchParams]); + + if (policy.loaded && policy.inviteOnly && !hasInviteFromUrl) { + return ; + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + try { + const res = await axios.post('/api/auth/register', { + username: username.trim(), + email: email.trim(), + password, + invitationCode: invitationCode.trim(), + }); + const data = res.data as { message?: string }; + setDone(data?.message || 'Your account is ready. Sign in to continue.'); + } catch (err) { + const e1 = err as { response?: { data?: { error?: string; msg?: string } } }; + setError(e1.response?.data?.error || e1.response?.data?.msg || 'Registration failed.'); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+
+ c + commonly +
+ + {done ? ( + <> +

Account created

+

{done}

+ + + ) : ( +
+

Create your account

+

+ Join the shared space where agents and humans collaborate. +

+ + + + + + + + + + {error &&
{error}
} + +
+ Already have an account? + {' '} + Sign in +
+
+ )} +
+
+ ); +}; + +export default V2Register; From 62248a5942a2b6daddd8170c640b1153c0127d05 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 1 Jun 2026 18:40:46 +0800 Subject: [PATCH 2/2] fix(v2): correct V2Register card + submit button styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two visual bugs caught in browser verification: - The bare
picked up a dark global background — make the carry .v2-login__card (matching V2Login) so the card renders white. - .v2-login__submit lost to the global button reset (transparent fill) — scope it as .v2-root button.v2-login__submit so the accent applies. Also fixes the pre-existing latent unstyled submit button on V2Login. Co-Authored-By: Claude Opus 4.8 --- frontend/src/v2/components/V2Register.tsx | 165 ++++++++++++---------- frontend/src/v2/v2.css | 9 +- 2 files changed, 93 insertions(+), 81 deletions(-) diff --git a/frontend/src/v2/components/V2Register.tsx b/frontend/src/v2/components/V2Register.tsx index 9541d6db..23769d45 100644 --- a/frontend/src/v2/components/V2Register.tsx +++ b/frontend/src/v2/components/V2Register.tsx @@ -7,12 +7,22 @@ import axios from '../../utils/axiosConfig'; // Register flow: honor the invite-only policy, POST /api/auth/register, then // surface the backend's message and hand off to sign-in (the backend may send // a verification email; it does not always return a usable session). +// +// The .v2-login__card class goes on the /
directly (like V2Login) — +// a bare picks up a dark global background, so it must carry the card. interface RegistrationPolicy { loaded: boolean; inviteOnly: boolean; } +const Brand: React.FC = () => ( +
+ c + commonly +
+); + const V2Register: React.FC = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -60,87 +70,86 @@ const V2Register: React.FC = () => { } }; + if (done) { + return ( +
+
+ +

Account created

+

{done}

+ +
+
+ ); + } + return (
-
-
- c - commonly + + +

Create your account

+

+ Join the shared space where agents and humans collaborate. +

+ + + + + + + + + + {error &&
{error}
} + +
+ Already have an account? + {' '} + Sign in
- - {done ? ( - <> -

Account created

-

{done}

- - - ) : ( - -

Create your account

-

- Join the shared space where agents and humans collaborate. -

- - - - - - - - - - {error &&
{error}
} - -
- Already have an account? - {' '} - Sign in -
- - )} -
+
); }; diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index cfd9bdf1..eaef9e15 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -3381,7 +3381,10 @@ border-color: var(--v2-accent); } -.v2-login__submit { +/* Specificity-matched against the global .v2-root button:not(.MuiButtonBase-root) + reset (0,0,2,1) — prefix with .v2-root button.X so the accent fill wins. + Without this the submit button renders transparent (login + register). */ +.v2-root button.v2-login__submit { width: 100%; margin-top: 8px; padding: 10px 14px; @@ -3393,11 +3396,11 @@ transition: background 80ms ease; } -.v2-login__submit:hover:not(:disabled) { +.v2-root button.v2-login__submit:hover:not(:disabled) { background: var(--v2-accent-strong); } -.v2-login__submit:disabled { +.v2-root button.v2-login__submit:disabled { background: var(--v2-border-strong); cursor: not-allowed; }