diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index 80bcbcd..4acb6a2 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -3,30 +3,7 @@ import { supabase } from '../supabaseClient'; import { Link } from 'react-router-dom'; import { motion as Motion } from 'motion/react'; import { Mail, Lock, User, ArrowRight, AlertCircle, CheckCircle2, Eye, EyeOff } from 'lucide-react'; - -/* eslint-disable react-refresh/only-export-components -- test-only helper exports */ -// Generous 5 min: a false "already exists" for a genuinely new user is worse -// than missing a duplicate. -export const EXISTING_ACCOUNT_AGE_MS = 300_000; - -export const DUPLICATE_ACCOUNT_MESSAGE = - "An account with this email already exists — log in or reset your password."; - -// Supabase returns identities: [] when the email is already registered (confirmed -// account). For an unconfirmed duplicate, identities come back non-empty but the -// account's created_at is old (Supabase re-signup doesn't reset it). A missing or -// unparseable created_at, or missing identities, is treated as a fresh account. -export function isExistingAccount(user, now = Date.now()) { - if (!user) return false; - if (user.identities?.length === 0) return true; - if (user.created_at) { - const createdAt = Date.parse(user.created_at); - if (!Number.isNaN(createdAt) && now - createdAt > EXISTING_ACCOUNT_AGE_MS) { - return true; - } - } - return false; -} +import { isExistingAccount, DUPLICATE_ACCOUNT_MESSAGE } from '../utils/registerHelpers'; export default function Register() { const [email, setEmail] = useState(''); @@ -44,7 +21,7 @@ export default function Register() { const oauthError = localStorage.getItem('oauth_login_error'); if (oauthError) { setError(oauthError); - setErrorKind(null); + setErrorKind(oauthError.includes('already exists') ? 'duplicate' : null); localStorage.removeItem('oauth_login_error'); } }, []); diff --git a/frontend/src/pages/__tests__/Register.test.jsx b/frontend/src/pages/__tests__/Register.test.jsx index e2ebf26..a0225d2 100644 --- a/frontend/src/pages/__tests__/Register.test.jsx +++ b/frontend/src/pages/__tests__/Register.test.jsx @@ -4,7 +4,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup, act } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; -import Register, { isExistingAccount, EXISTING_ACCOUNT_AGE_MS } from '../Register'; +import Register from '../Register'; +import { isExistingAccount, EXISTING_ACCOUNT_AGE_MS } from '../../utils/registerHelpers'; // Mock Supabase vi.mock('../../supabaseClient', () => ({ @@ -496,6 +497,14 @@ describe('Register', () => { expect(localStorage.getItem('oauth_login_error')).toBeNull(); }); + it('shows log-in link for OAuth existing-account error from localStorage', () => { + localStorage.setItem('oauth_login_error', 'An account with this Google account already exists. Please login instead.'); + + renderRegister(); + + expect(screen.getByRole('link', { name: /^log in$/i })).toBeInTheDocument(); + }); + it('handles Google OAuth error', async () => { const { supabase } = await import('../../supabaseClient'); supabase.auth.signInWithOAuth.mockResolvedValue({ diff --git a/frontend/src/utils/registerHelpers.js b/frontend/src/utils/registerHelpers.js new file mode 100644 index 0000000..f88c041 --- /dev/null +++ b/frontend/src/utils/registerHelpers.js @@ -0,0 +1,24 @@ +// Pure helpers for Register: duplicate-account detection after supabase.auth.signUp. + +// Generous 5 min: a false "already exists" for a genuinely new user is worse +// than missing a duplicate. +export const EXISTING_ACCOUNT_AGE_MS = 300_000; + +export const DUPLICATE_ACCOUNT_MESSAGE = + "An account with this email already exists — log in or reset your password."; + +// Supabase returns identities: [] when the email is already registered (confirmed +// account). For an unconfirmed duplicate, identities come back non-empty but the +// account's created_at is old (Supabase re-signup doesn't reset it). A missing or +// unparseable created_at, or missing identities, is treated as a fresh account. +export function isExistingAccount(user, now = Date.now()) { + if (!user) return false; + if (user.identities?.length === 0) return true; + if (user.created_at) { + const createdAt = Date.parse(user.created_at); + if (!Number.isNaN(createdAt) && now - createdAt > EXISTING_ACCOUNT_AGE_MS) { + return true; + } + } + return false; +}