SHiNE-server/shine-UI/js/pages/registration-payment-view.js

260 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { renderHeader } from '../components/header.js';
import {
authService,
setAuthError,
setAuthInfo,
state,
} from '../state.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import {
deriveWalletFromPassword,
formatSol,
getBalanceSol,
getTopupSiteUrl,
} from '../services/solana-wallet-service.js';
import { registerUserOnSolana } from '../services/solana-register-service.js';
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
const MIN_REQUIRED_SOL = 0.01;
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 };
}
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 draftPassword = String(state.registrationDraft.password ?? '');
const wallet = await deriveWalletFromPassword(draftPassword);
const address = String(wallet?.address || '').trim();
if (!address) throw new Error('Не удалось вычислить адрес wallet.key');
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;
}
// Используем предсгенерированный keyBundle или генерируем заново
let keyBundle = state.registrationDraft.preGeneratedKeyBundle;
if (!keyBundle) {
keyBundle = await authService.derivePasswordKeyBundle(
state.registrationDraft.login,
state.registrationDraft.password,
);
}
// Регистрация на Solana (смарт контракт)
submitButton.textContent = 'Регистрация в Solana...';
try {
await registerUserOnSolana({
login: state.registrationDraft.login,
keyBundle,
solanaEndpoint: state.entrySettings.solanaServer,
});
} catch (solanaError) {
const solanaMsg = String(solanaError?.message || '');
// Пользователь уже зарегистрирован в Solana — продолжаем
if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) {
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
}
}
// Регистрация на сервере SHiNE
submitButton.textContent = 'Регистрация на сервере...';
await authService.reconnect(state.entrySettings.shineServer);
const result = await authService.registerUser(state.registrationDraft.login, state.registrationDraft.password);
state.registrationDraft.flowType = 'registration';
state.registrationDraft.sessionId = result.sessionId;
state.registrationDraft.storagePwd = result.storagePwd;
state.registrationDraft.pendingKeyBundle = result.keyBundle;
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
setAuthInfo(`Регистрация завершена. Вы вошли как @${result.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 {
submitButton.disabled = false;
submitButton.textContent = 'Зарегистрироваться';
}
});
card.innerHTML = `
<p class="auth-copy">Для регистрации в тестовой Solana нужно минимум 0,01 SOL на вашем кошельке.</p>
<label class="stack"><span class="field-label">Номер кошелька (wallet.key = device.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 = `Не удалось подготовить wallet.key: ${error?.message || 'unknown'}`;
status.style.display = '';
}
})();
return screen;
}