import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Loader2 } from 'lucide-react'; import { useMutation } from '@tanstack/react-query'; import { ADMIN_DEFAULT_AFTER_LOGIN_PATH, ADMIN_EVENTS_PATH, ADMIN_PUBLIC_FORGOT_PASSWORD_PATH, ADMIN_PUBLIC_HELP_PATH, } from '../constants'; import { useAuth } from '../auth/context'; import { resolveReturnTarget } from '../lib/returnTo'; import { useInstallPrompt } from './hooks/useInstallPrompt'; import { getInstallBannerDismissed, setInstallBannerDismissed, shouldShowInstallBanner } from './lib/installBanner'; import { MobileInstallBanner } from './components/MobileInstallBanner'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Button } from '@tamagui/button'; import { MobileCard } from './components/Primitives'; import { MobileField, MobileInput } from './components/FormControls'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; type LoginResponse = { token: string; token_type: string; abilities: string[]; }; async function performLogin(payload: { login: string; password: string; return_to?: string | null }): Promise { const response = await fetch('/api/v1/tenant-auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ ...payload, remember: true, }), }); if (response.status === 422) { const data = await response.json(); const errors = data.errors ?? {}; const flattened = Object.values(errors).flat(); throw new Error(flattened.join(' ') || 'Validation failed'); } if (!response.ok) { throw new Error('Login failed.'); } return (await response.json()) as LoginResponse; } export default function MobileLoginPage() { const { status, applyToken, abilities } = useAuth(); const { t } = useTranslation('auth'); const location = useLocation(); const navigate = useNavigate(); const installPrompt = useInstallPrompt(); const { text, muted, primary, dangerBg, dangerText, border, surface } = useAdminTheme(); const safeAreaStyle: React.CSSProperties = { paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)', paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; const searchParams = React.useMemo(() => new URLSearchParams(location.search), [location.search]); const rawReturnTo = searchParams.get('return_to'); const computeDefaultAfterLogin = React.useCallback( (abilityList?: string[]) => { const source = abilityList ?? abilities; return source.includes('tenant-admin') ? ADMIN_DEFAULT_AFTER_LOGIN_PATH : ADMIN_EVENTS_PATH; }, [abilities], ); const fallbackTarget = computeDefaultAfterLogin(); const { finalTarget } = React.useMemo( () => resolveReturnTarget(rawReturnTo, fallbackTarget), [rawReturnTo, fallbackTarget], ); React.useEffect(() => { if (status === 'authenticated') { navigate(finalTarget, { replace: true }); } }, [finalTarget, navigate, status]); const [login, setLogin] = React.useState(''); const [password, setPassword] = React.useState(''); const [error, setError] = React.useState(null); const [installBannerDismissed, setInstallBannerDismissedState] = React.useState(() => getInstallBannerDismissed()); const installBanner = shouldShowInstallBanner( { isInstalled: installPrompt.isInstalled, isStandalone: installPrompt.isStandalone, canInstall: installPrompt.canInstall, isIos: installPrompt.isIos, }, installBannerDismissed, ); const mutation = useMutation({ mutationKey: ['tenantAdminLoginMobile'], mutationFn: performLogin, onError: (err: unknown) => { const message = err instanceof Error ? err.message : String(err); setError(message); }, onSuccess: async (data) => { setError(null); await applyToken(data.token, data.abilities ?? []); const postLoginFallback = computeDefaultAfterLogin(data.abilities ?? []); const { finalTarget: successTarget } = resolveReturnTarget(rawReturnTo, postLoginFallback); navigate(successTarget, { replace: true }); }, }); const isSubmitting = (mutation as { isPending?: boolean; isLoading?: boolean }).isPending ?? (mutation as { isPending?: boolean; isLoading?: boolean }).isLoading ?? false; const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); setError(null); mutation.mutate({ login, password, return_to: rawReturnTo, }); }; return ( {t('auth.logoAlt', {t('login.panel_title', 'Fotospiel.App Event Login')} {t('login.panel_copy', 'Melde dich an, um Events zu planen, Fotos zu moderieren und Aufgaben anzulegen.')}
setLogin(e.target.value)} placeholder={t('login.email_placeholder', 'name@example.com')} required /> setPassword(e.target.value)} placeholder={t('login.password_placeholder', '••••••••')} required /> {error ? ( {error} ) : null}
void installPrompt.promptInstall() : undefined} onDismiss={() => { setInstallBannerDismissed(true); setInstallBannerDismissedState(true); }} /> {t('login.support', 'Fragen? Schreib uns an support@fotospiel.de oder antworte direkt auf deine Einladung.')}
); }