// Profile page — show linked providers, allow linking email + TG const { useState: useProfileState, useEffect: useProfileEffect } = React; // IMPORTANT: defined at module scope (not inside ProfilePage). Defining it // inside the parent created a NEW component type on every parent render, // which made React unmount/remount the inputs underneath — focus dropped // after each keystroke (https://react.dev/learn/preserving-and-resetting-state). function ProviderCard({ title, icon, linked, linkedLabel, children, linkedChildren }) { return (
{icon}
{title}
{linked &&
{linkedLabel}
}
{linked ? ✓ Привязан : Не привязан }
{!linked &&
{children}
} {linked && linkedChildren &&
{linkedChildren}
}
); } function ProfilePage({ user, onNavigate, botConfig }) { const [providers, setProviders] = useProfileState([]); const [emailInput, setEmailInput] = useProfileState(''); const [emailPass, setEmailPass] = useProfileState(''); const [emailConfirm, setEmailConfirm] = useProfileState(''); const [emailCode, setEmailCode] = useProfileState(''); const [emailStep, setEmailStep] = useProfileState('idle'); // idle | sent | done const [emailLoading, setEmailLoading] = useProfileState(false); const [emailError, setEmailError] = useProfileState(''); const [changePwOpen, setChangePwOpen] = useProfileState(false); const [changePwCurrent, setChangePwCurrent] = useProfileState(''); const [changePwNew, setChangePwNew] = useProfileState(''); const [changePwConfirm, setChangePwConfirm] = useProfileState(''); const [changePwLoading, setChangePwLoading] = useProfileState(false); const [changePwError, setChangePwError] = useProfileState(''); const [changePwSuccess, setChangePwSuccess] = useProfileState(false); const [tgInput, setTgInput] = useProfileState(''); const [tgCode, setTgCode] = useProfileState(''); const [tgStep, setTgStep] = useProfileState('idle'); // idle | sent | done const [tgError, setTgError] = useProfileState(''); useProfileEffect(() => { api.get('/api/me/providers').then(data => { if (!data.__unauthorized) setProviders(data); }).catch(() => {}); }, []); const emailProvider = providers.find(p => p.provider === 'email'); const tgProvider = providers.find(p => p.provider === 'telegram'); const handleLinkEmailRequest = async () => { if (!emailInput || !emailPass) return setEmailError('Заполните email и пароль'); if (emailPass.length < 8) return setEmailError('Пароль — минимум 8 символов'); if (emailPass !== emailConfirm) return setEmailError('Пароли не совпадают'); setEmailLoading(true); setEmailError(''); try { await api.post('/api/auth/link/email/request', { email: emailInput, password: emailPass, password_confirm: emailConfirm }); setEmailStep('sent'); } catch (e) { if (e.status === 409) setEmailError('Email уже привязан к другому аккаунту'); else if (e.status === 429) setEmailError('Подождите перед повторной отправкой'); else setEmailError(e.message || 'Ошибка отправки кода'); } finally { setEmailLoading(false); } }; const handleLinkEmailVerify = async () => { if (!emailCode || emailCode.length < 6) return; setEmailLoading(true); setEmailError(''); try { await api.post('/api/auth/link/email/verify', { email: emailInput, code: emailCode }); setEmailStep('done'); setProviders(prev => [...prev, { provider: 'email', identifier: emailInput, created_at: new Date().toISOString(), last_used_at: null }]); } catch (e) { if (e.status === 410) setEmailError('Код истёк — запросите новый'); else if (e.status === 401) setEmailError('Неверный код'); else if (e.status === 409) setEmailError('Email уже привязан к другому аккаунту'); else setEmailError(e.message || 'Ошибка проверки кода'); } finally { setEmailLoading(false); } }; const handleChangePassword = async () => { if (!changePwCurrent || !changePwNew) return setChangePwError('Заполните все поля'); if (changePwNew.length < 8) return setChangePwError('Новый пароль — минимум 8 символов'); if (changePwNew !== changePwConfirm) return setChangePwError('Пароли не совпадают'); setChangePwLoading(true); setChangePwError(''); try { await api.post('/api/auth/change-password', { current_password: changePwCurrent, new_password: changePwNew, new_password_confirm: changePwConfirm, }); setChangePwSuccess(true); setChangePwOpen(false); setChangePwCurrent(''); setChangePwNew(''); setChangePwConfirm(''); } catch (e) { if (e.status === 401) setChangePwError('Неверный текущий пароль'); else setChangePwError(e.message || 'Ошибка смены пароля'); } finally { setChangePwLoading(false); } }; const isValidPhone = (raw) => { const cleaned = (raw || '').replace(/[^\d+]/g, ''); if (/^\+\d{10,15}$/.test(cleaned)) return true; if (/^\d{10,11}$/.test(cleaned)) return true; return false; }; const handleRequestTgCode = async () => { if (!tgInput) return; if (!isValidPhone(tgInput)) { setTgError('Введите номер телефона, например +79001234567'); return; } setTgError(''); try { await api.post('/api/auth/link/telegram/request-code', { identifier: tgInput }); setTgStep('sent'); } catch (e) { if (e.status === 429) { const sec = e.retry_after; setTgError(sec ? `Подождите ${sec} секунд перед повторной отправкой` : 'Подождите перед повторной отправкой'); } else if (e.status === 400) { // Backend message already mentions /connect; bot deep-link rendered separately. setTgError(e.message || 'Не удалось найти ваш Telegram по этому номеру.'); } else if (e.status === 502) { setTgError('Не удалось отправить код через Telegram. Попробуйте позже.'); } else { setTgError(e.message || 'Ошибка отправки кода'); } } }; const handleVerifyTg = async () => { if (!tgCode || tgCode.length < 6) return; setTgError(''); try { await api.post('/api/auth/link/telegram/verify-code', { identifier: tgInput, code: tgCode }); setTgStep('done'); setProviders(prev => [...prev, { provider: 'telegram', identifier: tgInput, created_at: new Date().toISOString(), last_used_at: null }]); } catch (e) { if (e.status === 410) setTgError('Код истёк — запросите новый'); else if (e.status === 401) setTgError('Неверный код'); else if (e.status === 409) setTgError( 'Этот Telegram уже привязан к другому аккаунту. ' + 'Войдите в тот аккаунт по номеру и отвяжите Telegram там, ' + 'либо напишите в поддержку — поможем разобраться.' ); else setTgError(e.message || 'Ошибка проверки кода'); } }; return (

Профиль

Управляйте способами входа в аккаунт

{(user?.first_name || '?')[0].toUpperCase()}
{user?.first_name || 'Пользователь'}
@{user?.user_name || 'username'}

Способы входа

{changePwSuccess && !changePwOpen && (
Пароль изменён
)} {!changePwOpen ? ( ) : (
{changePwError &&
{changePwError}
}
setChangePwCurrent(e.target.value)} />
setChangePwNew(e.target.value)} />
setChangePwConfirm(e.target.value)} />
)}
)} > {emailStep === 'done' ? (
✅ Email успешно привязан
) : emailStep === 'idle' ? (
{emailError &&
{emailError}
}
setEmailInput(e.target.value)} />
setEmailPass(e.target.value)} />
setEmailConfirm(e.target.value)} />
Придумайте пароль для входа через email
) : (
{emailError &&
{emailError}
}
Код подтверждения отправлен на {emailInput}
setEmailCode(e.target.value.replace(/\D/g, ''))} style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.15em', fontWeight: 700 }} autoFocus />
)} {tgStep === 'done' ? (
✅ Telegram успешно привязан
) : tgStep === 'idle' ? (
{tgError && (
{tgError}
)}
setTgInput(e.target.value)} />
Если бот ещё не знает ваш номер,{' '} откройте @{(botConfig && botConfig.bot_username) || 'AVITOPF_bot'} {' '}— он сразу попросит поделиться контактом
) : (
{tgError &&
{tgError}
}
Код отправлен в Telegram на {tgInput}
setTgCode(e.target.value.replace(/\D/g, ''))} style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.15em', fontWeight: 700 }} autoFocus />
)}
); } Object.assign(window, { ProfilePage });