import { useState } from "react"; import { Alert, Anchor, Button, Checkbox, Divider, Group, PasswordInput, Stack, Text, TextInput, Title, UnstyledButton, } from "@mantine/core"; import { IconArrowLeft, IconArrowRight, IconDeviceMobile, IconLock, IconMail, } from "@tabler/icons-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useNavigate, Link } from "react-router-dom"; import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { useApiMutation } from "@ema-platform/api"; import { notify, useErrorHandler } from "@ema-platform/ui"; import { AuthShell } from "../components/AuthShell"; import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice"; import type { LoginPayload, AuthUser, CurrentProfile, } from "../types/auth.types"; import { useAuthConfig } from "../AuthConfig"; import { authStorage } from "../utils/auth-storage"; export function LoginPage() { const navigate = useNavigate(); const dispatch = useDispatch(); const { t } = useTranslation(); const { appName, loginRedirectPath, enableSignup, enableForgotPassword } = useAuthConfig(); const [isLoading, setIsLoading] = useState(false); const [rememberMe, setRememberMe] = useState(true); const [serverError, setServerError] = useState(null); const { handleError } = useErrorHandler(); const [loginTrigger] = useApiMutation(); const [meTrigger] = useApiMutation(); const [profileTrigger] = useApiMutation<{ profile: CurrentProfile | null }>(); const handleBack = () => { if (window.history.length > 1) { navigate(-1); } else { navigate("/"); } }; // Built inside the component (not module scope) so validation messages // pick up the active language — same pattern as ProfilePage's forms. const schema = z.object({ email: z .string() .trim() .transform((value) => { // Convert 09xxxxxxxx -> +2519xxxxxxxx if (/^09\d{8}$/.test(value)) { return `+251${value.substring(1)}`; } return value; }) .refine( (value) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^\+2519\d{8}$/; return emailRegex.test(value) || phoneRegex.test(value); }, { message: t( "login.emailOrPhoneInvalid", "Enter a valid email or phone number (+2519xxxxxxxx)", ), }, ), password: z.string().min(8, { message: t("login.passwordMinLength", "Password must be at least 8 characters"), }), }); type FormValues = z.infer; const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(schema), }); const onSubmit = async (values: FormValues) => { setIsLoading(true); try { const data = await loginTrigger({ url: "/auth/login", method: "POST", body: values, }).unwrap(); dispatch(loginSuccess(data)); const me = await meTrigger({ url: "/auth/me", method: "GET", }).unwrap(); dispatch(setUser(me)); // Warm the profile so screens that need an id have one on first paint. // `/profiles/me` provisions an empty profile when the user has none, so // unlike the old filtered lookup this cannot come back empty-handed. // Sign-in is still never gated on it — a failure here is ignored and // `useCurrentProfile` resolves it again on demand. try { const { profile } = await profileTrigger({ url: "/profiles/me", method: "GET", }).unwrap(); if (profile) { authStorage.setProfileId(profile.id); dispatch(setCurrentProfile(profile)); } } catch { // Offline or a 5xx — the portal still works; the resolver retries. } if (!me.isPhoneNumberVerified) { navigate("/otp-verify", { state: { email: me.email, phoneNumber: me.phoneNumber, }, }); return; } navigate(loginRedirectPath); } catch (err: unknown) { setServerError(handleError(err)); } finally { localStorage.setItem("rememberMe", String(rememberMe)); setIsLoading(false); } }; return ( { e.currentTarget.style.color = "var(--mantine-primary-color-filled)"; e.currentTarget.style.transform = "translateX(-3px)"; }} onMouseLeave={(e) => { e.currentTarget.style.color = "var(--mantine-color-dimmed)"; e.currentTarget.style.transform = "translateX(0)"; }} > {t("common.back", "Back")}
{t("login.welcome", { appName, defaultValue: "Welcome to {{appName}}" })} {t("login.subtitle", "Sign in to access your account.")}
{serverError && ( setServerError(null)} > {serverError} )}
} error={errors.email?.message} {...register("email")} /> } error={errors.password?.message} {...register("password")} /> setRememberMe(e.currentTarget.checked)} /> {enableForgotPassword && ( {t("login.forgotPassword", "Forgot password?")} )}
{enableSignup && ( {t("login.noAccount", "Don't have an account? ")} {t("login.createOne", "Create one")} )}
); }