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..23769d45 --- /dev/null +++ b/frontend/src/v2/components/V2Register.tsx @@ -0,0 +1,157 @@ +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). +// +// 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(); + 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); + } + }; + + if (done) { + return ( +
+
+ +

Account created

+

{done}

+ +
+
+ ); + } + + return ( +
+ + +

Create your account

+

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

+ + + + + + + + + + {error &&
{error}
} + +
+ Already have an account? + {' '} + Sign in +
+ +
+ ); +}; + +export default V2Register; 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; }