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 oauthError = searchParams.get('error'); const oauthDescription = searchParams.get('error_description'); 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, encodedFinal } = React.useMemo( () => resolveReturnTarget(rawReturnTo, fallbackTarget), [rawReturnTo, fallbackTarget], ); const oauthMessage = React.useMemo(() => { if (!oauthError) { return null; } const fallback = oauthDescription ?? oauthError; return t(`login.oauth_errors.${oauthError}`, { defaultValue: fallback }); }, [oauthDescription, oauthError, t]); const googleHref = React.useMemo(() => { if (!encodedFinal) { return '/event-admin/auth/google'; } const params = new URLSearchParams({ return_to: encodedFinal }); return `/event-admin/auth/google?${params.toString()}`; }, [encodedFinal]); const facebookHref = React.useMemo(() => { if (!encodedFinal) { return '/event-admin/auth/facebook'; } const params = new URLSearchParams({ return_to: encodedFinal }); return `/event-admin/auth/facebook?${params.toString()}`; }, [encodedFinal]); 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 [isRedirectingToGoogle, setIsRedirectingToGoogle] = React.useState(false); const [isRedirectingToFacebook, setIsRedirectingToFacebook] = React.useState(false); 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 isFormValid = login.trim().length > 0 && password.length > 0; const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); setError(null); if (!isFormValid) { return; } mutation.mutate({ login, password, return_to: rawReturnTo, }); }; const handleGoogleLogin = React.useCallback(() => { if (typeof window === 'undefined') { return; } setIsRedirectingToGoogle(true); window.location.assign(googleHref); }, [googleHref]); const handleFacebookLogin = React.useCallback(() => { if (typeof window === 'undefined') { return; } setIsRedirectingToFacebook(true); window.location.assign(facebookHref); }, [facebookHref]); 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 Fotoaufgaben anzulegen.')}
{oauthMessage ? ( {t('login.oauth_error_title', 'Login aktuell nicht moeglich')} {oauthMessage} ) : null} 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.')}
); } function GoogleIcon({ size = 18 }: { size?: number }) { return ( ); } function FacebookIcon({ size = 18 }: { size?: number }) { return ( ); }