import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { loginSchema, type LoginFormData } from './schemas'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { PasswordInput } from '@/features/auth/components/PasswordInput'; import { AuthLayout } from '@/features/auth/components/AuthLayout'; import { AlertCircle } from 'lucide-react'; import { useNavigate, useSearch } from '@tanstack/react-router'; import { authApi } from '@/infrastructure/http/api/auth'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import { authenticatedUserStore } from '@/infrastructure/storage/AuthenticatedUserStore'; import { useTranslation } from '@/i18n/TranslationProvider'; import { WpRegisterPage } from './WpRegisterPage'; /** * LoginPage Component * * Renders the TWWIM-branded login form with: * - Email field with validation * - Password field with validation * - TWWIM branding (primary color #0284c7) * - Responsive design (mobile-first) * * Uses React Hook Form with Zod schema validation. */ const isWpEmbed = import.meta.env.VITE_WP_EMBED === 'true'; export function LoginPage() { const { t, locale } = useTranslation(); const landingUrl = import.meta.env.VITE_LANDING_URL || ''; const navigate = useNavigate(); const search = useSearch({ from: '/login' }) as { redirect?: string }; const [showWpRegister, setShowWpRegister] = useState(false); // Wipe stale tokens + user snapshot once on mount. MUST NOT run in the // component body — react-hook-form re-renders the page after onSubmit // resolves (isSubmitting flips back to false) and a body-level clear would // race the `_authenticated` guard's in-memory read of authenticatedUserStore, // redirecting the user straight back to /login in WP hash-history mode. useEffect(() => { tokenStorage.removeTokens(); authenticatedUserStore.clear(); }, []); const { register, handleSubmit, setError, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(loginSchema), }); if (isWpEmbed && showWpRegister) { return setShowWpRegister(false)} />; } const onSubmit = async (data: LoginFormData) => { try { const { user, session, authenticatedUser } = await authApi.login({ email: data.email, password: data.password, }); // Tokens remain in LocalTokenStorage (archer_* keys, legacy). // AuthenticatedUser lives in its own twwim_* store. AuthenticatedUserMapper // already seeds authOrigin from the JWT inside fromTokenResponse, so we // just persist what the API layer returned — no manual JWT merge here. tokenStorage.setTokens(session.accessToken, session.refreshToken); authenticatedUserStore.set(authenticatedUser); console.log('Login successful:', user.email); // Navigate to redirect URL or dashboard (never redirect to auth pages) const raw = search.redirect || '/dashboard'; const redirectTo = raw.startsWith('/auth') || raw === '/login' ? '/dashboard' : raw; // replace:true — never leave /login in the hash-history back stack so // Back/Forward doesn't drop a freshly-authenticated user onto the login // page, which would then mount-clear their tokens. navigate({ to: redirectTo, replace: true }); } catch (error) { console.error('Login failed:', error); // Handle specific error types if (error instanceof Error) { if (error.message.includes('2FA')) { setError('root', { message: t('auth.error2FA') }); } else if (error.message.includes('Invalid') || error.message.includes('credentials')) { setError('root', { message: t('auth.errorInvalidCredentials') }); } else { setError('root', { message: error.message || t('auth.errorLoginFailed') }); } } else { setError('root', { message: t('auth.errorUnexpected') }); } } }; return ( {/* Login Form */}
{/* Form-level Error Alert (T3.1) */} {errors.root && ( {errors.root.message} )} {/* Email Field (T3.5 - Enhanced attributes) */}
{errors.email && ( )}
{/* Password Field (T3.2 - PasswordInput component, T3.3 - Forgot Password link) */}
{t('auth.forgotPassword')}
{errors.password && ( )}
{/* Submit Button (T3.4 - Glow effect on hover) */}
{/* Footer with sign-up link */}
{t('auth.noAccount')}{' '} {isWpEmbed ? ( ) : ( {t('auth.signUp')} )}
); }