diff --git a/e2e/journeys/j1-registration-login.spec.ts b/e2e/journeys/j1-registration-login.spec.ts index ec116ab..b1b28a4 100644 --- a/e2e/journeys/j1-registration-login.spec.ts +++ b/e2e/journeys/j1-registration-login.spec.ts @@ -10,7 +10,6 @@ test.describe('J1: Registration and Login', () => { await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!'); await page.click('button[type="submit"]'); - // With VITE_MOCK_AUTH=true the app navigates to "/" on success await expect(page).toHaveURL('http://localhost:5173/'); await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible(); }); diff --git a/playwright.config.ts b/playwright.config.ts index b22d74a..a2d7b0b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }, ], webServer: { - command: 'VITE_MOCK_AUTH=true npm run dev', + command: 'npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, }, diff --git a/src/App.tsx b/src/App.tsx index ee4c2dc..953553a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { AccountLinking } from './pages/AccountLinking.tsx' import { Login } from './pages/Login.tsx' import { Register } from './pages/Register.tsx' import { ForgotPassword } from './pages/ForgotPassword.tsx' +import { VerifyEmail } from './pages/VerifyEmail.tsx' const queryClient = new QueryClient({ defaultOptions: { @@ -47,6 +48,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index cf92831..294ec4f 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,25 +1,8 @@ -import { useEffect } from 'react' import { Navigate, Outlet } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function ProtectedRoute() { - const isMockAuth = import.meta.env.VITE_MOCK_AUTH === 'true' const { data: session, isPending } = authClient.useSession() - const isAuthenticated = useAuthStore((s) => s.isAuthenticated) - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) - - useEffect(() => { - if (!isMockAuth) { - setAuthenticated(!!session) - } - }, [session, setAuthenticated, isMockAuth]) - - // In mock auth mode, rely on Zustand store (set by Login/Register pages) - if (isMockAuth) { - if (!isAuthenticated) return - return - } if (isPending) { return ( diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index ae7fc0c..5044613 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function Login() { const [email, setEmail] = useState('') @@ -9,7 +8,6 @@ export function Login() { const [error, setError] = useState('') const [loading, setLoading] = useState(false) const navigate = useNavigate() - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -40,12 +38,7 @@ export function Login() { setError('Sign in failed. Please try again.') } } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - setAuthenticated(true) - navigate('/') - } else { - setError('Invalid email or password. Please try again.') - } + setError('Invalid email or password. Please try again.') } finally { setLoading(false) } diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c75e2d6..ba039ba 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function Register() { const [name, setName] = useState('') @@ -9,8 +8,10 @@ export function Register() { const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) + const [registrationComplete, setRegistrationComplete] = useState(false) + const [resendLoading, setResendLoading] = useState(false) + const [resendMessage, setResendMessage] = useState('') const navigate = useNavigate() - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -38,27 +39,57 @@ export function Register() { throw new Error(authError.message ?? 'Registration failed') } - // After successful signUp, force a session fetch to confirm the cookie is set - // before navigating to the protected route - const sessionResult = await authClient.getSession() - if (sessionResult.data) { - navigate('/') - } else { - // Session not established — show success message and link to login - setError('Account created! Please sign in.') - } + setRegistrationComplete(true) } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - setAuthenticated(true) - navigate('/') - } else { - setError('Registration failed. Please try again.') - } + setError('Registration failed. Please try again.') } finally { setLoading(false) } } + async function handleResendVerification() { + setResendLoading(true) + setResendMessage('') + try { + const { error } = await authClient.sendVerificationEmail({ email }) + if (error) { + setResendMessage('Failed to resend. Please try again.') + } else { + setResendMessage('Verification email sent!') + } + } finally { + setResendLoading(false) + } + } + + if (registrationComplete) { + return ( +
+

Check your email

+

+ We sent a verification link to {email}. Click it to activate your account. +

+ + {resendMessage && ( +

{resendMessage}

+ )} +

+ Already have an account?{' '} + + Sign in + +

+
+ ) + } + return (

Create Account

diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx new file mode 100644 index 0000000..d1c5fb3 --- /dev/null +++ b/src/pages/VerifyEmail.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { authClient } from "../lib/auth-client.ts"; + +export function VerifyEmail() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [status, setStatus] = useState<"verifying" | "success" | "error">("verifying"); + const [resendEmail, setResendEmail] = useState(""); + const [showResend, setShowResend] = useState(false); + const [resending, setResending] = useState(false); + const [resendMessage, setResendMessage] = useState(""); + + useEffect(() => { + const token = searchParams.get("token"); + const callbackURL = searchParams.get("callbackURL") || "/"; + + if (!token) { + setStatus("error"); + return; + } + + authClient.verifyEmail({ query: { token } }) + .then(() => { + setStatus("success"); + setTimeout(() => { + navigate(callbackURL); + }, 2000); + }) + .catch(() => { + setStatus("error"); + }); + }, [searchParams, navigate]); + + async function handleResend() { + if (!resendEmail) { + setResendMessage("Please enter your email address."); + return; + } + + setResending(true); + setResendMessage(""); + + try { + const { error } = await authClient.sendVerificationEmail({ email: resendEmail }); + if (error) { + setResendMessage("Failed to resend. Please try again."); + } else { + setResendMessage("Verification email sent!"); + setShowResend(false); + } + } finally { + setResending(false); + } + } + + return ( +
+ {status === "verifying" && ( + <> +
+

Verifying your email...

+

Please wait while we verify your email address.

+ + )} + + {status === "success" && ( + <> +

Email verified!

+

Redirecting you shortly...

+ + )} + + {status === "error" && ( + <> +

Verification failed

+

The verification link may have expired or is invalid.

+ + {!showResend ? ( + + ) : ( +
+ setResendEmail(e.target.value)} + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + + {resendMessage && ( +

{resendMessage}

+ )} +
+ )} + + )} +
+ ); +}