SHA256
560 lines
23 KiB
JavaScript
560 lines
23 KiB
JavaScript
import { renderHeader } from '../components/header.js';
|
|
import {
|
|
authService,
|
|
setAuthError,
|
|
setAuthInfo,
|
|
state,
|
|
} from '../state.js';
|
|
import { clearStoredMessages } from '../services/message-store.js';
|
|
import { toUserMessage } from '../services/ui-error-texts.js';
|
|
import {
|
|
formatSol,
|
|
getBalanceSol,
|
|
getTopupSiteUrl,
|
|
} from '../services/solana-wallet-service.js';
|
|
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
|
import {
|
|
checkLoginExistsOnSolana,
|
|
formatSolanaErrorDetails,
|
|
isUserAlreadyExistsSolanaError,
|
|
registerUserOnSolana,
|
|
} from '../services/solana-register-service.js';
|
|
import { defaultServerLogin } from '../deploy-config.js';
|
|
|
|
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
|
const MIN_REQUIRED_SOL = 0.01;
|
|
const REGISTRATION_PROGRESS_DURATION_MS = 12000;
|
|
const REGISTRATION_POLL_START_DELAY_MS = 4000;
|
|
const REGISTRATION_POLL_INTERVAL_MS = 2000;
|
|
const REGISTRATION_CONFIRM_TIMEOUT_MS = 25000;
|
|
const REGISTRATION_SUCCESS_SETTLE_DELAY_MS = 2000;
|
|
|
|
function getExplorerClusterName(endpoint) {
|
|
const source = String(endpoint || '').trim().toLowerCase();
|
|
if (!source) return 'mainnet-beta';
|
|
if (source.includes('devnet')) return 'devnet';
|
|
if (source.includes('testnet')) return 'testnet';
|
|
return 'mainnet-beta';
|
|
}
|
|
|
|
function makeSolanaExplorerTxUrl(signature, endpoint) {
|
|
const cleanSignature = String(signature || '').trim();
|
|
if (!cleanSignature) return '';
|
|
const cluster = getExplorerClusterName(endpoint);
|
|
const url = new URL(`https://explorer.solana.com/tx/${encodeURIComponent(cleanSignature)}`);
|
|
if (cluster !== 'mainnet-beta') {
|
|
url.searchParams.set('cluster', cluster);
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function parseBalanceSol(value) {
|
|
const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function getCryptoRuntimeState() {
|
|
const hasCrypto = Boolean(globalThis.crypto);
|
|
const hasGetRandomValues = Boolean(globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function');
|
|
const hasSubtle = Boolean(globalThis.crypto && (globalThis.crypto.subtle || globalThis.crypto.webkitSubtle));
|
|
const secureContext = window.isSecureContext === true;
|
|
return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext };
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
async function prepareRegistrationLoginForKeyStorage({ keyBundle }) {
|
|
await authService.reconnect(state.entrySettings.shineServer);
|
|
const result = await authService.createSessionForExistingUser(
|
|
state.registrationDraft.login,
|
|
state.registrationDraft.password,
|
|
);
|
|
state.registrationDraft.flowType = 'register';
|
|
state.registrationDraft.login = result.login;
|
|
state.registrationDraft.sessionId = result.sessionId;
|
|
state.registrationDraft.storagePwd = result.storagePwd;
|
|
state.registrationDraft.pendingKeyBundle = keyBundle || result.keyBundle;
|
|
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
|
return result;
|
|
}
|
|
|
|
export function render({ navigate }) {
|
|
const screen = document.createElement('section');
|
|
screen.className = 'stack';
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'card stack';
|
|
|
|
const status = document.createElement('p');
|
|
status.className = 'status-line is-unavailable';
|
|
status.style.display = 'none';
|
|
|
|
const walletValue = document.createElement('input');
|
|
walletValue.className = 'input';
|
|
walletValue.type = 'text';
|
|
walletValue.value = state.registrationPayment.walletAddress || '';
|
|
walletValue.readOnly = true;
|
|
|
|
const walletRow = document.createElement('div');
|
|
walletRow.className = 'inline-input-row';
|
|
|
|
const copyButton = document.createElement('button');
|
|
copyButton.className = 'ghost-btn';
|
|
copyButton.type = 'button';
|
|
copyButton.textContent = 'Скопировать номер';
|
|
copyButton.addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(walletValue.value);
|
|
copyButton.textContent = 'Скопировано';
|
|
window.setTimeout(() => {
|
|
copyButton.textContent = 'Скопировать номер';
|
|
}, 1500);
|
|
} catch {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = 'Не удалось скопировать номер кошелька.';
|
|
status.style.display = '';
|
|
}
|
|
});
|
|
|
|
walletRow.append(walletValue, copyButton);
|
|
|
|
const balanceRow = document.createElement('div');
|
|
balanceRow.className = 'row wrap-row';
|
|
|
|
const balanceValue = document.createElement('strong');
|
|
balanceValue.textContent = `${formatSol(parseBalanceSol(state.registrationPayment.balanceSOL), 6)} SOL`;
|
|
|
|
const refreshButton = document.createElement('button');
|
|
refreshButton.className = 'square-btn';
|
|
refreshButton.type = 'button';
|
|
refreshButton.textContent = '↻';
|
|
refreshButton.title = 'Обновить';
|
|
|
|
const refreshBalance = async ({ showError = true, addressOverride = '' } = {}) => {
|
|
const address = String(addressOverride || walletValue.value || '').trim();
|
|
if (!address) return null;
|
|
refreshButton.disabled = true;
|
|
try {
|
|
const balance = await getBalanceSol({
|
|
endpoint: state.entrySettings.solanaServer,
|
|
address,
|
|
});
|
|
state.registrationPayment.balanceSOL = String(balance.sol);
|
|
balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`;
|
|
return Number(balance.sol) || 0;
|
|
} catch (error) {
|
|
if (showError) {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = `Не удалось обновить баланс: ${error?.message || 'unknown'}`;
|
|
status.style.display = '';
|
|
}
|
|
return null;
|
|
} finally {
|
|
refreshButton.disabled = false;
|
|
}
|
|
};
|
|
|
|
const deriveUserWalletAddress = async () => {
|
|
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
|
|
if (!keyBundle) throw new Error('Ключи ещё не сгенерированы. Вернитесь на предыдущий шаг.');
|
|
const { publicKeyB64 } = keyBundle.clientPair;
|
|
const raw = atob(publicKeyB64);
|
|
const bytes = new Uint8Array(raw.length);
|
|
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
|
const { PublicKey } = await loadSolanaWeb3();
|
|
const address = new PublicKey(bytes).toBase58();
|
|
state.registrationPayment.walletAddress = address;
|
|
walletValue.value = address;
|
|
return address;
|
|
};
|
|
|
|
refreshButton.addEventListener('click', () => {
|
|
void refreshBalance();
|
|
});
|
|
|
|
balanceRow.append(balanceValue, refreshButton);
|
|
|
|
const topupButton = document.createElement('button');
|
|
topupButton.className = 'ghost-btn';
|
|
topupButton.type = 'button';
|
|
topupButton.textContent = 'Пополнить кошелёк';
|
|
topupButton.addEventListener('click', async () => {
|
|
try {
|
|
const walletAddress = await deriveUserWalletAddress();
|
|
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
|
|
} catch (error) {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = `Не удалось подготовить кошелёк: ${error?.message || 'unknown'}`;
|
|
status.style.display = '';
|
|
}
|
|
});
|
|
|
|
const showKeysButton = document.createElement('button');
|
|
showKeysButton.className = 'ghost-btn';
|
|
showKeysButton.type = 'button';
|
|
showKeysButton.textContent = 'Показать сгенерированные ключи';
|
|
showKeysButton.addEventListener('click', () => navigate('registration-draft-keys-view'));
|
|
|
|
const submitButton = document.createElement('button');
|
|
submitButton.className = 'primary-btn';
|
|
submitButton.type = 'button';
|
|
submitButton.textContent = 'Зарегистрироваться';
|
|
submitButton.addEventListener('click', async () => {
|
|
status.style.display = 'none';
|
|
|
|
const cryptoState = getCryptoRuntimeState();
|
|
if (!cryptoState.hasCrypto || !cryptoState.hasGetRandomValues || !cryptoState.hasSubtle) {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = 'Криптография браузера недоступна. Откройте приложение через HTTPS tunnel или localhost и повторите регистрацию.';
|
|
status.style.display = '';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
submitButton.disabled = true;
|
|
submitButton.textContent = 'Регистрация...';
|
|
|
|
const walletAddress = await deriveUserWalletAddress();
|
|
const currentBalance = await refreshBalance({ showError: true, addressOverride: walletAddress });
|
|
if (currentBalance == null) return;
|
|
if (currentBalance < MIN_REQUIRED_SOL) {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = `Для регистрации нужно минимум ${MIN_REQUIRED_SOL} SOL. Сейчас на кошельке ${formatSol(currentBalance, 6)} SOL. Пополните на промо-странице или попросите перевод у знакомого с тестовыми SOL.`;
|
|
status.style.display = '';
|
|
const openTopup = window.confirm('Открыть страницу пополнения с вашим кошельком?');
|
|
if (openTopup) {
|
|
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
|
|
if (!keyBundle) throw new Error('Ключи не найдены. Вернитесь на предыдущий шаг.');
|
|
|
|
// Регистрация на Solana (смарт контракт)
|
|
submitButton.textContent = 'Регистрация в Solana...';
|
|
let registrationTxId = '';
|
|
try {
|
|
const registrationResult = await registerUserOnSolana({
|
|
login: state.registrationDraft.login,
|
|
keyBundle,
|
|
solanaEndpoint: state.entrySettings.solanaServer,
|
|
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
|
|
promoCode: '',
|
|
});
|
|
registrationTxId = String(registrationResult?.signature || '').trim();
|
|
} catch (solanaError) {
|
|
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
|
// Пользователь уже зарегистрирован в Solana — продолжаем
|
|
if (!solanaMsg.includes('already') && !isUserAlreadyExistsSolanaError(solanaError)) {
|
|
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
|
|
}
|
|
}
|
|
|
|
renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId });
|
|
} catch (error) {
|
|
const message = toUserMessage(error, 'Не удалось завершить регистрацию.');
|
|
setAuthError(message);
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = message;
|
|
status.style.display = '';
|
|
} finally {
|
|
submitButton.disabled = false;
|
|
submitButton.textContent = 'Зарегистрироваться';
|
|
}
|
|
});
|
|
|
|
card.innerHTML = `
|
|
<p class="auth-copy">Для регистрации в тестовой Solana нужно минимум 0,01 SOL на вашем кошельке.</p>
|
|
<label class="stack"><span class="field-label">Номер кошелька (client.key)</span></label>
|
|
<div class="stack">
|
|
<span class="field-label">Баланс (Solana)</span>
|
|
</div>
|
|
`;
|
|
card.children[1].append(walletRow);
|
|
card.children[2].append(balanceRow);
|
|
card.append(topupButton, showKeysButton, submitButton, status);
|
|
|
|
screen.append(
|
|
renderHeader({
|
|
title: 'Оплата регистрации',
|
|
leftAction: { label: '←', onClick: () => navigate('register-view') },
|
|
}),
|
|
card,
|
|
);
|
|
|
|
(async () => {
|
|
try {
|
|
const walletAddress = await deriveUserWalletAddress();
|
|
await refreshBalance({ addressOverride: walletAddress });
|
|
} catch (error) {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = `Не удалось подготовить client.key: ${error?.message || 'unknown'}`;
|
|
status.style.display = '';
|
|
}
|
|
})();
|
|
|
|
return screen;
|
|
}
|
|
|
|
function renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
|
const screen = document.querySelector('section.stack');
|
|
if (!screen) return;
|
|
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
|
const card = screen.querySelector('.card.stack');
|
|
if (!card) return;
|
|
card.classList.add('registration-finish-card');
|
|
|
|
let progressTimerId = null;
|
|
let pollStartTimerId = null;
|
|
let pollTimerId = null;
|
|
let successTimerId = null;
|
|
let confirmationInFlight = false;
|
|
let stageClosed = false;
|
|
let successPending = false;
|
|
let timeoutShown = false;
|
|
const stageStartedAt = Date.now();
|
|
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
|
|
|
const title = document.createElement('h2');
|
|
title.className = 'registration-finish-title';
|
|
title.textContent = 'Идёт регистрация в блокчейне...';
|
|
|
|
const hint = document.createElement('p');
|
|
hint.className = 'auth-copy registration-finish-text';
|
|
hint.textContent = 'Ждём, пока транзакция будет полностью одобрена сетью Solana.';
|
|
|
|
const txIdLine = document.createElement('p');
|
|
txIdLine.className = 'meta-muted registration-finish-tx';
|
|
if (registrationTxId && txExplorerUrl) {
|
|
const txLabel = document.createElement('span');
|
|
txLabel.textContent = 'Tx ID регистрации: ';
|
|
const txLink = document.createElement('a');
|
|
txLink.className = 'registration-finish-tx-link';
|
|
txLink.href = txExplorerUrl;
|
|
txLink.target = '_blank';
|
|
txLink.rel = 'noopener noreferrer';
|
|
txLink.textContent = registrationTxId;
|
|
txIdLine.append(txLabel, txLink);
|
|
} else {
|
|
txIdLine.textContent = 'Tx ID регистрации: ожидаем подтверждённую подпись';
|
|
}
|
|
|
|
const progressWrap = document.createElement('div');
|
|
progressWrap.className = 'registration-progress';
|
|
|
|
const progressBar = document.createElement('div');
|
|
progressBar.className = 'registration-progress-bar';
|
|
progressWrap.append(progressBar);
|
|
|
|
const progress = document.createElement('p');
|
|
progress.className = 'meta-muted registration-finish-progress';
|
|
progress.textContent = 'Подготавливаем проверку подтверждения...';
|
|
|
|
const pollStatus = document.createElement('p');
|
|
pollStatus.className = 'meta-muted registration-finish-progress';
|
|
pollStatus.textContent = 'Через несколько секунд начнём проверять подтверждение регистрации.';
|
|
|
|
const stopTimers = () => {
|
|
if (progressTimerId) {
|
|
window.clearInterval(progressTimerId);
|
|
progressTimerId = null;
|
|
}
|
|
if (pollStartTimerId) {
|
|
window.clearTimeout(pollStartTimerId);
|
|
pollStartTimerId = null;
|
|
}
|
|
if (pollTimerId) {
|
|
window.clearInterval(pollTimerId);
|
|
pollTimerId = null;
|
|
}
|
|
if (successTimerId) {
|
|
window.clearTimeout(successTimerId);
|
|
successTimerId = null;
|
|
}
|
|
};
|
|
|
|
const updateVisualProgress = () => {
|
|
const elapsed = Date.now() - stageStartedAt;
|
|
let widthPercent = 0;
|
|
if (elapsed <= REGISTRATION_PROGRESS_DURATION_MS) {
|
|
const phaseRatio = clamp(elapsed / REGISTRATION_PROGRESS_DURATION_MS, 0, 1);
|
|
widthPercent = 100 * (1 - ((1 - phaseRatio) ** 1.85));
|
|
} else {
|
|
const extraRatio = 1 - Math.exp(-(elapsed - REGISTRATION_PROGRESS_DURATION_MS) / 9000);
|
|
widthPercent = 88 + (10 * clamp(extraRatio, 0, 1));
|
|
}
|
|
progressBar.style.width = `${clamp(widthPercent, 0, 98)}%`;
|
|
|
|
if (elapsed < REGISTRATION_POLL_START_DELAY_MS) {
|
|
progress.textContent = 'Отправили регистрацию в сеть. Даём транзакции несколько секунд на обработку.';
|
|
return;
|
|
}
|
|
if (elapsed < REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
|
progress.textContent = 'Идёт ожидание подтверждения Solana. Индикатор может замедлиться, это нормально.';
|
|
return;
|
|
}
|
|
progress.textContent = 'Подтверждение затянулось. Продолжаем автоматическую проверку.';
|
|
};
|
|
|
|
const finalizeSuccess = () => {
|
|
if (stageClosed || successPending) return;
|
|
successPending = true;
|
|
stopTimers();
|
|
progress.textContent = 'Подтверждение получено. Завершаем регистрацию...';
|
|
pollStatus.textContent = 'Ещё секунду: запись уже найдена в Solana, синхронизируем финальный экран.';
|
|
status.style.display = 'none';
|
|
progressBar.style.transition = `width ${REGISTRATION_SUCCESS_SETTLE_DELAY_MS}ms ease-out`;
|
|
progressBar.style.width = '100%';
|
|
successTimerId = window.setTimeout(() => {
|
|
if (stageClosed) return;
|
|
stageClosed = true;
|
|
renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId });
|
|
}, REGISTRATION_SUCCESS_SETTLE_DELAY_MS);
|
|
};
|
|
|
|
const showTimeoutState = () => {
|
|
if (timeoutShown || stageClosed) return;
|
|
timeoutShown = true;
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = 'Подтверждение не пришло вовремя. Возможно, регистрация уже прошла, а возможно ещё нет. Проверьте Tx ID ниже: мы продолжим автоматическую проверку.';
|
|
status.style.display = '';
|
|
pollStatus.textContent = 'Пока подтверждения нет. Подождите ещё секундочку, мы продолжаем проверять регистрацию.';
|
|
};
|
|
|
|
const tryCheckRegistration = async () => {
|
|
if (stageClosed || confirmationInFlight) return;
|
|
confirmationInFlight = true;
|
|
progress.textContent = 'Проверяем подтверждение регистрации в Solana...';
|
|
status.style.display = timeoutShown ? '' : 'none';
|
|
try {
|
|
const result = await checkLoginExistsOnSolana({
|
|
login: state.registrationDraft.login,
|
|
solanaEndpoint: state.entrySettings.solanaServer,
|
|
});
|
|
if (result?.exists) {
|
|
finalizeSuccess();
|
|
return;
|
|
}
|
|
pollStatus.textContent = 'Пока ещё не прошла, подождите ещё секундочку...';
|
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
|
showTimeoutState();
|
|
}
|
|
} catch (error) {
|
|
console.warn('Registration confirmation check failed', toUserMessage(error, 'registration-confirmation'));
|
|
pollStatus.textContent = 'Проверяем регистрацию повторно...';
|
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
|
showTimeoutState();
|
|
}
|
|
} finally {
|
|
confirmationInFlight = false;
|
|
}
|
|
};
|
|
|
|
if (headerBackButton) {
|
|
const replacement = headerBackButton.cloneNode(true);
|
|
replacement.addEventListener('click', () => {
|
|
stageClosed = true;
|
|
stopTimers();
|
|
navigate('start-view');
|
|
});
|
|
headerBackButton.replaceWith(replacement);
|
|
}
|
|
|
|
card.innerHTML = '';
|
|
status.style.display = 'none';
|
|
card.append(title, hint, txIdLine, progressWrap, progress, pollStatus, status);
|
|
|
|
updateVisualProgress();
|
|
progressTimerId = window.setInterval(() => {
|
|
if (stageClosed) return;
|
|
updateVisualProgress();
|
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
|
showTimeoutState();
|
|
}
|
|
}, 160);
|
|
|
|
pollStartTimerId = window.setTimeout(() => {
|
|
if (stageClosed) return;
|
|
void tryCheckRegistration();
|
|
pollTimerId = window.setInterval(() => {
|
|
void tryCheckRegistration();
|
|
}, REGISTRATION_POLL_INTERVAL_MS);
|
|
}, REGISTRATION_POLL_START_DELAY_MS);
|
|
}
|
|
|
|
function renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
|
const screen = document.querySelector('section.stack');
|
|
if (!screen) return;
|
|
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
|
const card = screen.querySelector('.card.stack');
|
|
if (!card) return;
|
|
|
|
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
|
let loginInFlight = false;
|
|
|
|
const title = document.createElement('h2');
|
|
title.className = 'registration-finish-title';
|
|
title.textContent = 'Поздравляем с регистрацией';
|
|
|
|
const hint = document.createElement('p');
|
|
hint.className = 'auth-copy registration-finish-text';
|
|
hint.textContent = 'Регистрация подтверждена в блокчейне Solana. Теперь можно войти в аккаунт и выбрать, какие ключи сохранить на устройстве.';
|
|
|
|
const txIdLine = document.createElement('p');
|
|
txIdLine.className = 'meta-muted registration-finish-tx';
|
|
if (registrationTxId && txExplorerUrl) {
|
|
const txLabel = document.createElement('span');
|
|
txLabel.textContent = 'Tx ID регистрации: ';
|
|
const txLink = document.createElement('a');
|
|
txLink.className = 'registration-finish-tx-link';
|
|
txLink.href = txExplorerUrl;
|
|
txLink.target = '_blank';
|
|
txLink.rel = 'noopener noreferrer';
|
|
txLink.textContent = registrationTxId;
|
|
txIdLine.append(txLabel, txLink);
|
|
} else {
|
|
txIdLine.textContent = 'Регистрация подтверждена. Tx ID недоступен.';
|
|
}
|
|
|
|
const enterButton = document.createElement('button');
|
|
enterButton.className = 'primary-btn';
|
|
enterButton.type = 'button';
|
|
enterButton.textContent = 'Войти в аккаунт';
|
|
enterButton.addEventListener('click', async () => {
|
|
if (loginInFlight) return;
|
|
loginInFlight = true;
|
|
status.className = 'status-line';
|
|
status.textContent = 'Готовим вход и экран сохранения ключей...';
|
|
status.style.display = '';
|
|
enterButton.disabled = true;
|
|
try {
|
|
await prepareRegistrationLoginForKeyStorage({ keyBundle });
|
|
await clearStoredMessages().catch(() => {});
|
|
setAuthInfo(`Регистрация подтверждена для @${state.registrationDraft.login}. Выберите, какие ключи сохранить на устройстве.`);
|
|
navigate('registration-keys-view');
|
|
} catch (error) {
|
|
const message = toUserMessage(error, 'Не удалось подготовить вход после регистрации.');
|
|
setAuthError(message);
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = message;
|
|
status.style.display = '';
|
|
} finally {
|
|
loginInFlight = false;
|
|
enterButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
if (headerBackButton) {
|
|
const replacement = headerBackButton.cloneNode(true);
|
|
replacement.addEventListener('click', () => navigate('start-view'));
|
|
headerBackButton.replaceWith(replacement);
|
|
}
|
|
|
|
card.innerHTML = '';
|
|
status.style.display = 'none';
|
|
card.append(title, hint, txIdLine, enterButton, status);
|
|
}
|