import { loginWithMagicLink, signUpWithMagicLink, verifyMagicLinkOtp, } from '@chat-app/shared/auth'; import { extractErrorCode, isSupportedLocale, type SupportedLocale, } from '@chat-app/shared/i18n'; import { useCallback, useId, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate } from 'react-router-dom'; import { AlertIcon, ArrowRightIcon, AtIcon, CheckCircleIcon, LockIcon, LogoMark, MailIcon, ShieldIcon, SparklesIcon, SpinnerIcon, TicketIcon, } from '../components/icons'; import { LanguageSwitcher } from '../components/LanguageSwitcher'; import { useAuth } from '../context/AuthContext'; import { env } from '../lib/env'; import { supabase } from '../lib/supabase'; type Mode = 'signup' | 'login'; type UiState = | { kind: 'idle' } | { kind: 'sending' } | { kind: 'sent'; email: string } | { kind: 'error'; message: string }; const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,32}$/; export function AuthPage() { const { session } = useAuth(); const { t, i18n } = useTranslation(['auth', 'common', 'errors']); const [ui, setUi] = useState({ kind: 'idle' }); const [mode, setMode] = useState('signup'); const [email, setEmail] = useState(''); const [username, setUsername] = useState(''); const [inviteCode, setInviteCode] = useState('DEV-INVITE-001'); const usernameValid = useMemo(() => USERNAME_PATTERN.test(username), [username]); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); setUi({ kind: 'sending' }); try { if (mode === 'signup') { const activeLocale = i18n.resolvedLanguage ?? i18n.language; await signUpWithMagicLink(supabase, { email, username, inviteCode, redirectTo: env.authRedirectUrl, ...(isSupportedLocale(activeLocale) ? { locale: activeLocale satisfies SupportedLocale } : {}), }); } else { await loginWithMagicLink(supabase, email, env.authRedirectUrl); } setUi({ kind: 'sent', email }); } catch (err: unknown) { const code = extractErrorCode(err); const message = code ? t('errors:' + code, { defaultValue: t('errors:generic') }) : err instanceof Error ? err.message : t('errors:generic'); setUi({ kind: 'error', message }); } }, [mode, email, username, inviteCode, i18n, t], ); if (session) return ; return (
); } // --------------------------------------------------------------------------- // Shell background — ambient blobs spread across the full viewport. Anchored // to percentages so they stay in the same relative position regardless of // monitor width (works as well on 1440 as on 3440 ultrawide). // --------------------------------------------------------------------------- function ShellBackground() { return (