/** * Login Client Component * * Handles client-side login flow using the LoginFlowController. * Deterministic state machine per docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md */ 'use client'; import { useAuth } from '@/components/auth/AuthContext'; import { LoginFlowController, LoginState } from '@/lib/auth/LoginFlowController'; import { LoginViewModelBuilder } from '@/lib/builders/view-models/LoginViewModelBuilder'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; import { LoginMutation } from '@/lib/mutations/auth/LoginMutation'; import { validateLoginForm, type LoginFormValues } from '@/lib/utils/validation'; import { LoginViewData } from '@/lib/view-data/LoginViewData'; import { LoginViewModel } from '@/lib/view-models/auth/LoginViewModel'; import { LoginLoadingTemplate } from '@/templates/auth/LoginLoadingTemplate'; import { LoginTemplate } from '@/templates/auth/LoginTemplate'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; export function LoginClient({ viewData }: ClientWrapperProps) { const router = useRouter(); const searchParams = useSearchParams(); const { refreshSession, session } = useAuth(); // Build ViewModel from ViewData const [viewModel, setViewModel] = useState(() => LoginViewModelBuilder.build(viewData) ); // Login flow controller const controller = useMemo(() => { const returnTo = searchParams.get('returnTo') ?? '/dashboard'; return new LoginFlowController(session, returnTo); }, [session, searchParams]); // Check controller state on mount and session changes useEffect(() => { const action = controller.getNextAction(); if (action.type === 'REDIRECT') { router.replace(action.path); } }, [controller, router]); // Handle form field changes const handleChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; const checked = 'checked' in e.target ? e.target.checked : false; const fieldValue = type === 'checkbox' ? checked : value; setViewModel(prev => { const newFormState = { ...prev.formState, fields: { ...prev.formState.fields, [name]: { ...prev.formState.fields[name as keyof typeof prev.formState.fields], value: fieldValue, touched: true, error: undefined, }, }, }; return prev.withFormState(newFormState); }); }; // Handle form submission const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const values: LoginFormValues = { email: viewModel.formState.fields.email.value as string, password: viewModel.formState.fields.password.value as string, rememberMe: viewModel.formState.fields.rememberMe.value as boolean, }; // Validate form const errors = validateLoginForm(values); const hasErrors = Object.keys(errors).length > 0; if (hasErrors) { setViewModel(prev => { const newFormState = { ...prev.formState, isValid: false, submitCount: prev.formState.submitCount + 1, fields: { ...prev.formState.fields, email: { ...prev.formState.fields.email, error: errors.email, touched: true, }, password: { ...prev.formState.fields.password, error: errors.password, touched: true, }, }, }; return prev.withFormState(newFormState); }); return; } // Update submitting state setViewModel(prev => prev.withMutationState(true, null)); try { // Execute login mutation const mutation = new LoginMutation(); const result = await mutation.execute({ email: values.email, password: values.password, rememberMe: values.rememberMe, }); if (result.isErr()) { const error = result.getError(); setViewModel(prev => { const newFormState = { ...prev.formState, isSubmitting: false, submitError: error, }; return prev.withFormState(newFormState).withMutationState(false, error); }); if (process.env.NODE_ENV === 'development') { setViewModel(prev => prev.withUIState({ ...prev.uiState, showErrorDetails: true, })); } return; } // Success - refresh session and transition await refreshSession(); // Transition to post-auth state controller.transitionToPostAuth(); const action = controller.getNextAction(); if (action.type === 'REDIRECT') { router.push(action.path); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Login failed'; setViewModel(prev => { const newFormState = { ...prev.formState, isSubmitting: false, submitError: errorMessage, }; return prev.withFormState(newFormState).withMutationState(false, errorMessage); }); if (process.env.NODE_ENV === 'development') { setViewModel(prev => prev.withUIState({ ...prev.uiState, showErrorDetails: true, })); } } }; // Toggle password visibility const togglePassword = () => { setViewModel(prev => prev.withUIState({ ...prev.uiState, showPassword: !prev.uiState.showPassword, })); }; // Get current state from controller const state = controller.getState(); // If user is authenticated with permissions, show loading if (state === LoginState.AUTHENTICATED_WITH_PERMISSIONS) { return ; } // If user has insufficient permissions, show permission error if (state === LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS) { return ( { if (typeof state === 'function') { const newState = state(viewModel.formState); setViewModel(prev => prev.withFormState(newState)); } else { setViewModel(prev => prev.withFormState(state)); } }, setShowPassword: togglePassword, setShowErrorDetails: (show) => { setViewModel(prev => prev.withUIState({ ...prev.uiState, showErrorDetails: show, })); }, }} mutationState={{ isPending: viewModel.mutationPending, error: viewModel.mutationError, }} /> ); } // Show login form return ( { if (typeof state === 'function') { const newState = state(viewModel.formState); setViewModel(prev => prev.withFormState(newState)); } else { setViewModel(prev => prev.withFormState(state)); } }, setShowPassword: togglePassword, setShowErrorDetails: (show) => { setViewModel(prev => prev.withUIState({ ...prev.uiState, showErrorDetails: show, })); }, }} mutationState={{ isPending: viewModel.mutationPending, error: viewModel.mutationError, }} /> ); }