// 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 (