From f2972d023cd6e3480e5c127ebf1ae82612b3bf06 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 27 Jan 2026 17:50:03 +0300 Subject: [PATCH] Update Login.tsx --- src/pages/Login.tsx | 485 ++++++++++++++++++++++++++------------------ 1 file changed, 284 insertions(+), 201 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 6dea259..06139f2 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,235 +1,259 @@ -import { useState, useEffect, useMemo, useCallback } from 'react' -import { useNavigate, useLocation, useSearchParams } from 'react-router-dom' -import { useTranslation } from 'react-i18next' -import { useQuery } from '@tanstack/react-query' -import { useAuthStore } from '../store/auth' -import { authApi } from '../api/auth' -import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo, type EmailAuthEnabled } from '../api/branding' -import { getAndClearReturnUrl } from '../utils/token' -import LanguageSwitcher from '../components/LanguageSwitcher' -import TelegramLoginButton from '../components/TelegramLoginButton' +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { useAuthStore } from '../store/auth'; +import { authApi } from '../api/auth'; +import { + brandingApi, + getCachedBranding, + setCachedBranding, + preloadLogo, + isLogoPreloaded, + type BrandingInfo, + type EmailAuthEnabled, +} from '../api/branding'; +import { getAndClearReturnUrl } from '../utils/token'; +import LanguageSwitcher from '../components/LanguageSwitcher'; +import TelegramLoginButton from '../components/TelegramLoginButton'; export default function Login() { - const { t } = useTranslation() - const navigate = useNavigate() - const location = useLocation() - const [searchParams] = useSearchParams() - const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore() + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore(); // Extract referral code from URL - const referralCode = searchParams.get('ref') || '' + const referralCode = searchParams.get('ref') || ''; + const [activeTab, setActiveTab] = useState<'telegram' | 'email'>(() => + referralCode ? 'email' : 'telegram', + ); const [authMode, setAuthMode] = useState<'login' | 'register'>(() => - referralCode ? 'register' : 'login' - ) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [firstName, setFirstName] = useState('') - const [error, setError] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) - const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()) - const [registeredEmail, setRegisteredEmail] = useState(null) - const [showForgotPassword, setShowForgotPassword] = useState(false) - const [forgotPasswordEmail, setForgotPasswordEmail] = useState('') - const [forgotPasswordSent, setForgotPasswordSent] = useState(false) - const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false) - const [forgotPasswordError, setForgotPasswordError] = useState('') + referralCode ? 'register' : 'login', + ); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isTelegramWebApp, setIsTelegramWebApp] = useState(false); + const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()); + const [registeredEmail, setRegisteredEmail] = useState(null); + const [showForgotPassword, setShowForgotPassword] = useState(false); + const [forgotPasswordEmail, setForgotPasswordEmail] = useState(''); + const [forgotPasswordSent, setForgotPasswordSent] = useState(false); + const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false); + const [forgotPasswordError, setForgotPasswordError] = useState(''); // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { // Сначала проверяем state от React Router - const stateFrom = (location.state as { from?: string })?.from + const stateFrom = (location.state as { from?: string })?.from; if (stateFrom && stateFrom !== '/login') { - return stateFrom + return stateFrom; } // Затем проверяем сохранённый URL в sessionStorage (от safeRedirectToLogin) - const savedUrl = getAndClearReturnUrl() + const savedUrl = getAndClearReturnUrl(); if (savedUrl && savedUrl !== '/login') { - return savedUrl + return savedUrl; } // По умолчанию на главную - return '/' - }, [location.state]) + return '/'; + }, [location.state]); // Fetch branding with unified cache - const cachedBranding = useMemo(() => getCachedBranding(), []) + const cachedBranding = useMemo(() => getCachedBranding(), []); const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: async () => { - const data = await brandingApi.getBranding() - setCachedBranding(data) - preloadLogo(data) - return data + const data = await brandingApi.getBranding(); + setCachedBranding(data); + preloadLogo(data); + return data; }, staleTime: 60000, initialData: cachedBranding ?? undefined, - }) + }); // Check if email auth is enabled const { data: emailAuthConfig } = useQuery({ queryKey: ['email-auth-enabled'], queryFn: brandingApi.getEmailAuthEnabled, staleTime: 60000, - }) - const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true + }); + const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true; - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '' + const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; // If email auth is disabled but user came with ref param, redirect to bot useEffect(() => { if (referralCode && emailAuthConfig?.enabled === false && botUsername) { - window.location.href = `https://t.me/${botUsername}?start=ref_${referralCode}` + window.location.href = `https://t.me/${botUsername}?start=ref_${referralCode}`; } - }, [referralCode, emailAuthConfig, botUsername]) + }, [referralCode, emailAuthConfig, botUsername]); - const appName = branding ? branding.name : (import.meta.env.VITE_APP_NAME || 'VPN') - const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V' - const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null + // If email auth is disabled but we initially set to email tab, switch back to telegram + useEffect(() => { + if (!isEmailAuthEnabled && activeTab === 'email') { + setActiveTab('telegram'); + } + }, [isEmailAuthEnabled, activeTab]); + const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN'; + const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'; + const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; // Set document title useEffect(() => { - document.title = appName || 'VPN' - }, [appName]) + document.title = appName || 'VPN'; + }, [appName]); useEffect(() => { if (isAuthenticated) { - navigate(getReturnUrl(), { replace: true }) + navigate(getReturnUrl(), { replace: true }); } - }, [isAuthenticated, navigate, getReturnUrl]) + }, [isAuthenticated, navigate, getReturnUrl]); // Try Telegram WebApp authentication on mount useEffect(() => { const tryTelegramAuth = async () => { - const tg = window.Telegram?.WebApp + const tg = window.Telegram?.WebApp; if (tg?.initData) { - setIsTelegramWebApp(true) - tg.ready() - tg.expand() - setIsLoading(true) + setIsTelegramWebApp(true); + tg.ready(); + tg.expand(); + setIsLoading(true); try { - await loginWithTelegram(tg.initData) - navigate(getReturnUrl(), { replace: true }) + await loginWithTelegram(tg.initData); + navigate(getReturnUrl(), { replace: true }); } catch (err) { // Log only status code to avoid leaking sensitive data - const status = (err as { response?: { status?: number } })?.response?.status - console.warn('Telegram auth failed with status:', status) - setError(t('auth.telegramRequired')) + const status = (err as { response?: { status?: number } })?.response?.status; + console.warn('Telegram auth failed with status:', status); + setError(t('auth.telegramRequired')); } finally { - setIsLoading(false) + setIsLoading(false); } } - } + }; - tryTelegramAuth() - }, [loginWithTelegram, navigate, t, getReturnUrl]) + tryTelegramAuth(); + }, [loginWithTelegram, navigate, t, getReturnUrl]); const handleEmailSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') + e.preventDefault(); + setError(''); // Валидация email - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email.trim() || !emailRegex.test(email.trim())) { - setError(t('auth.invalidEmail', 'Please enter a valid email address')) - return + setError(t('auth.invalidEmail', 'Please enter a valid email address')); + return; } if (authMode === 'register') { // Валидация для регистрации if (password !== confirmPassword) { - setError(t('auth.passwordMismatch', 'Passwords do not match')) - return + setError(t('auth.passwordMismatch', 'Passwords do not match')); + return; } if (password.length < 8) { - setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')) - return + setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')); + return; } } - setIsLoading(true) + setIsLoading(true); try { if (authMode === 'login') { - await loginWithEmail(email, password) - navigate(getReturnUrl(), { replace: true }) + await loginWithEmail(email, password); + navigate(getReturnUrl(), { replace: true }); } else { - const result = await registerWithEmail(email, password, firstName || undefined, referralCode || undefined) + const result = await registerWithEmail( + email, + password, + firstName || undefined, + referralCode || undefined, + ); // Show "check your email" screen - setRegisteredEmail(result.email) + setRegisteredEmail(result.email); } } catch (err: unknown) { - const error = err as { response?: { status?: number; data?: { detail?: string } } } - const status = error.response?.status - const detail = error.response?.data?.detail + const error = err as { response?: { status?: number; data?: { detail?: string } } }; + const status = error.response?.status; + const detail = error.response?.data?.detail; if (status === 400 && detail?.includes('already registered')) { - setError(t('auth.emailAlreadyRegistered', 'This email is already registered')) + setError(t('auth.emailAlreadyRegistered', 'This email is already registered')); } else if (status === 401 || status === 403) { if (detail?.includes('verify your email')) { - setError(t('auth.emailNotVerified', 'Please verify your email first')) + setError(t('auth.emailNotVerified', 'Please verify your email first')); } else { - setError(t('auth.invalidCredentials', 'Invalid email or password')) + setError(t('auth.invalidCredentials', 'Invalid email or password')); } } else if (status === 429) { - setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later')) + setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later')); } else { - setError(detail || t('common.error')) + setError(detail || t('common.error')); } } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; const handleForgotPassword = async (e: React.FormEvent) => { - e.preventDefault() - setForgotPasswordError('') + e.preventDefault(); + setForgotPasswordError(''); - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!forgotPasswordEmail.trim() || !emailRegex.test(forgotPasswordEmail.trim())) { - setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address')) - return + setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address')); + return; } - setForgotPasswordLoading(true) + setForgotPasswordLoading(true); try { - await authApi.forgotPassword(forgotPasswordEmail.trim()) - setForgotPasswordSent(true) + await authApi.forgotPassword(forgotPasswordEmail.trim()); + setForgotPasswordSent(true); } catch (err: unknown) { - const error = err as { response?: { status?: number; data?: { detail?: string } } } - const detail = error.response?.data?.detail - setForgotPasswordError(detail || t('common.error')) + const error = err as { response?: { status?: number; data?: { detail?: string } } }; + const detail = error.response?.data?.detail; + setForgotPasswordError(detail || t('common.error')); } finally { - setForgotPasswordLoading(false) + setForgotPasswordLoading(false); } - } + }; const closeForgotPasswordModal = () => { - setShowForgotPassword(false) - setForgotPasswordEmail('') - setForgotPasswordSent(false) - setForgotPasswordError('') - } + setShowForgotPassword(false); + setForgotPasswordEmail(''); + setForgotPasswordSent(false); + setForgotPasswordError(''); + }; return ( -
+
{/* Background gradient */}
{/* Language switcher */} -
+
-
+
{/* Logo */}
-
+
{/* Always show letter as fallback */} - + {appLogo} {/* Logo image with smooth fade-in */} @@ -237,26 +261,30 @@ export default function Login() { {appName setLogoLoaded(true)} /> )}
- {appName && ( -

- {appName} -

- )} -

- {t('auth.loginSubtitle')} -

+ {appName &&

{appName}

} +

{t('auth.loginSubtitle')}

{/* Referral Banner - only show when email auth is enabled */} {referralCode && isEmailAuthEnabled && ( -
+
- - + + {t('auth.referralInvite')}
@@ -267,25 +295,38 @@ export default function Login() { {/* Check Email Screen */} {registeredEmail ? (
-
- - +
+ +
-

+

{t('auth.checkEmail', 'Check your email')}

-

+

{t('auth.verificationSent', 'We sent a verification link to:')}

-

{registeredEmail}

-

- {t('auth.clickLinkToVerify', 'Click the link in the email to verify your account and log in.')} +

{registeredEmail}

+

+ {t( + 'auth.clickLinkToVerify', + 'Click the link in the email to verify your account and log in.', + )}

) : ( - /* Card */ -
- {error && ( -
- {error} -
- )} - - {/* Telegram auth — always visible */} -
-

- {t('auth.registerHint')} -

- - {isLoading && isTelegramWebApp ? ( -
-
-

{t('auth.authenticating')}

+ /* Card */ +
+ {/* Tabs */} + {isEmailAuthEnabled ? ( +
+ +
) : ( - - )} -
- - {/* Email auth — only when enabled */} - {isEmailAuthEnabled && ( - <> - {/* Divider */} -
-
- {t('auth.or', 'or')} -
+
+

Telegram

+ )} + {error && ( +
+ {error} +
+ )} + + {activeTab === 'telegram' || !isEmailAuthEnabled ? ( +
+

{t('auth.registerHint')}

+ + {isLoading && isTelegramWebApp ? ( +
+
+

{t('auth.authenticating')}

+
+ ) : ( + + )} +
+ ) : (
{/* Login / Register toggle */} -
+
@@ -447,26 +504,28 @@ export default function Login() { {/* Verification notice for registration */} {authMode === 'register' && (

- {t('auth.verificationEmailNotice', 'After registration, a verification email will be sent to your address')} + {t( + 'auth.verificationEmailNotice', + 'After registration, a verification email will be sent to your address', + )}

)} {/* Forgot password link - only for login */} {authMode === 'login' && ( -
+
)}
- - )} -
+ )} +
)}
@@ -474,28 +533,47 @@ export default function Login() { {showForgotPassword && (
-
+
{forgotPasswordSent ? (
-
- - +
+ +
-

+

{t('auth.checkEmail', 'Check your email')}

-

- {t('auth.passwordResetSent', 'If an account exists with this email, we sent password reset instructions.')} +

+ {t( + 'auth.passwordResetSent', + 'If an account exists with this email, we sent password reset instructions.', + )}

) : ( <> -

+

{t('auth.forgotPassword', 'Forgot password?')}

-

- {t('auth.forgotPasswordHint', 'Enter your email and we will send you instructions to reset your password.')} +

+ {t( + 'auth.forgotPasswordHint', + 'Enter your email and we will send you instructions to reset your password.', + )}

- + {forgotPasswordError && ( -
+
{forgotPasswordError}
)} @@ -537,7 +620,7 @@ export default function Login() { > {forgotPasswordLoading ? ( - + {t('common.loading')} ) : ( @@ -551,5 +634,5 @@ export default function Login() {
)}
- ) + ); }