import React, { useEffect, useMemo, useState } from "react"; import { usePage } from "@inertiajs/react"; import { useTranslation } from "react-i18next"; import toast from "react-hot-toast"; import { LoaderCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import InputError from "@/components/input-error"; import TextLink from "@/components/text-link"; declare const route: (name: string, params?: Record) => string; export interface AuthUserPayload { id?: number; email?: string; name?: string | null; pending_purchase?: boolean; } interface LoginFormProps { onSuccess?: (userData: AuthUserPayload | null) => void; canResetPassword?: boolean; locale?: string; packageId?: number | null; } type SharedPageProps = { locale?: string; }; type FieldErrors = Record; const metaCsrfToken = () => (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? ""; const xsrfCookieToken = () => { if (typeof document === "undefined") { return ""; } const match = document.cookie.match(/(?:^|; )XSRF-TOKEN=([^;]*)/); return match ? decodeURIComponent(match[1]) : ""; }; export default function LoginForm({ onSuccess, canResetPassword = true, locale, packageId }: LoginFormProps) { const page = usePage(); const { t } = useTranslation("auth"); const resolvedLocale = locale ?? page.props.locale ?? "de"; const loginEndpoint = '/checkout/login'; const canUseOauth = typeof packageId === "number" && !Number.isNaN(packageId); const googleHref = useMemo(() => { if (!canUseOauth) { return ""; } const params = new URLSearchParams({ package_id: String(packageId), locale: resolvedLocale, }); return `/checkout/auth/google?${params.toString()}`; }, [canUseOauth, packageId, resolvedLocale]); const facebookHref = useMemo(() => { if (!canUseOauth) { return ""; } const params = new URLSearchParams({ package_id: String(packageId), locale: resolvedLocale, }); return `/checkout/auth/facebook?${params.toString()}`; }, [canUseOauth, packageId, resolvedLocale]); const [values, setValues] = useState({ identifier: "", password: "", remember: false, }); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isRedirectingToGoogle, setIsRedirectingToGoogle] = useState(false); const [isRedirectingToFacebook, setIsRedirectingToFacebook] = useState(false); const [hasTriedSubmit, setHasTriedSubmit] = useState(false); const [shouldFocusError, setShouldFocusError] = useState(false); useEffect(() => { if (!hasTriedSubmit) { return; } const collected = Object.values(errors).filter(Boolean); if (collected.length > 0) { toast.error(collected.join(" \u2022 ")); } }, [errors, hasTriedSubmit]); useEffect(() => { if (!hasTriedSubmit || !shouldFocusError) { return; } const firstKey = Object.keys(errors).find((key) => Boolean(errors[key])); if (!firstKey) { return; } const field = document.querySelector(`[name="${firstKey}"]`); field?.scrollIntoView({ behavior: "smooth", block: "center" }); field?.focus(); setShouldFocusError(false); }, [errors, hasTriedSubmit, shouldFocusError]); const updateValue = (key: keyof typeof values, value: string | boolean) => { setValues((current) => ({ ...current, [key]: value })); setErrors((current) => ({ ...current, [key as string]: "" })); setShouldFocusError(false); }; const submit = async (event: React.FormEvent) => { event.preventDefault(); setHasTriedSubmit(true); setErrors({}); setIsSubmitting(true); try { const cookieToken = xsrfCookieToken(); const metaToken = metaCsrfToken(); const response = await fetch(loginEndpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", ...(cookieToken ? { "X-XSRF-TOKEN": cookieToken } : metaToken ? { "X-CSRF-TOKEN": metaToken } : {}), }, credentials: "same-origin", body: JSON.stringify({ identifier: values.identifier, password: values.password, remember: values.remember, locale: resolvedLocale, package_id: packageId ?? null, }), }); if (response.ok) { const payload = await response.json(); const nextUser: AuthUserPayload | null = payload?.user ?? payload?.data ?? null; toast.success(t("login.success_toast", "Login erfolgreich")); onSuccess?.(nextUser); setValues((current) => ({ ...current, password: "" })); setHasTriedSubmit(false); return; } if (response.status === 422) { const payload = await response.json(); const fieldErrors: FieldErrors = {}; const source = payload?.errors ?? {}; Object.entries(source).forEach(([key, message]) => { if (Array.isArray(message) && message.length > 0) { fieldErrors[key] = String(message[0]); } else if (typeof message === "string") { fieldErrors[key] = message; } }); setErrors(fieldErrors); setShouldFocusError(true); toast.error(t("login.failed_generic", "Ungueltige Anmeldedaten")); return; } toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten.")); setShouldFocusError(false); } catch (error) { console.error("Login request failed", error); toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten.")); } finally { setIsSubmitting(false); } }; const handleGoogleLogin = () => { if (!googleHref) { return; } setIsRedirectingToGoogle(true); window.location.href = googleHref; }; const handleFacebookLogin = () => { if (!facebookHref) { return; } setIsRedirectingToFacebook(true); window.location.href = facebookHref; }; return (
updateValue("identifier", event.target.value)} />
{canResetPassword && ( {t("login.forgot")} )}
updateValue("password", event.target.value)} />
updateValue("remember", Boolean(checked))} />
{canUseOauth && (
{t("login.oauth_divider", "oder")}
)} {Object.values(errors).some(Boolean) && (

{Object.values(errors).filter(Boolean).join(" \u2022 ")}

)}
); } function GoogleIcon({ className }: { className?: string }) { return ( ); } function FacebookIcon({ className }: { className?: string }) { return ( ); }