import { useEffect } from "react"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useForm } from "@tanstack/react-form"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { ArrowRight } from "lucide-react"; import { z } from "zod"; import { loginSchema, type AuthSession, type LoginInput } from "@rubis/shared"; import { api, ApiError } from "@/lib/api"; import { authStore } from "@/lib/auth"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Field } from "@/components/ui/Field"; import { Eyebrow } from "@/components/ui/Eyebrow"; import { Brand } from "@/components/brand/Brand"; import { Gem } from "@/components/brand/Gem"; import { SsoButton, AuthDivider } from "@/components/auth/SsoButton"; const ssoErrorEnum = z .enum(["denied", "state_mismatch", "error", "no_email"]) .optional(); const searchSchema = z.object({ redirect: z.string().optional(), google: ssoErrorEnum, microsoft: ssoErrorEnum, }); const SSO_ERROR_MESSAGES: Record> = { google: { denied: "Connexion Google annulée.", state_mismatch: "Session expirée, réessayez la connexion Google.", error: "Connexion Google impossible. Réessayez dans un instant.", no_email: "Votre compte Google n'a pas d'email associé.", }, microsoft: { denied: "Connexion Microsoft annulée.", state_mismatch: "Session expirée, réessayez la connexion Microsoft.", error: "Connexion Microsoft impossible. Réessayez dans un instant.", no_email: "Votre compte Microsoft n'a pas d'email associé.", }, }; export const Route = createFileRoute("/login")({ validateSearch: searchSchema, component: LoginPage, }); function LoginPage() { const navigate = useNavigate(); const search = Route.useSearch(); // Toast d'erreur si on revient d'un échec SSO (?google=denied, ?microsoft=…). useEffect(() => { for (const provider of ["google", "microsoft"] as const) { const code = search[provider]; if (code && SSO_ERROR_MESSAGES[provider]?.[code]) { toast.error(SSO_ERROR_MESSAGES[provider]![code]!); } } }, [search.google, search.microsoft]); const loginMutation = useMutation({ mutationFn: async (input: LoginInput) => api.post("/api/v1/auth/login", input, { anonymous: true }), onSuccess: (session) => { authStore.setSession(session.accessToken, session.user); toast.success(`Bonjour ${session.user.fullName.split(" ")[0]}.`); // Si on a une URL de redirection (depuis le guard d'auth), on la suit ; // sinon / qui décide quoi faire selon l'état d'onboarding. void navigate({ to: search.redirect ?? "/" }); }, onError: (error: unknown) => { if (error instanceof ApiError && error.status === 401) { toast.error("Email ou mot de passe incorrect."); return; } toast.error("Connexion impossible. Réessayez dans un instant."); }, }); const form = useForm({ defaultValues: { email: "", password: "" } satisfies LoginInput, validators: { onChange: loginSchema, }, onSubmit: async ({ value }) => { await loginMutation.mutateAsync(value); }, }); return (
{/* Glow rubis discret en haut-droite — signature visuelle (cohérent landing). */}
); }