Обновить UI кошелька и регистрацию

This commit is contained in:
AidarKC 2026-06-24 13:48:07 +04:00
parent 684f3237cf
commit 77f5759d60
8 changed files with 275 additions and 172 deletions

View File

@ -30,7 +30,9 @@ const state = {
devicesResolvedAtMs: 0, devicesResolvedAtMs: 0,
}, },
currentWallet: null, currentWallet: null,
pendingApproval: null, pendingApprovals: [],
siteApprovalChain: Promise.resolve(),
sessionAttachInProgress: false,
statusText: '', statusText: '',
statusKind: 'info', statusKind: 'info',
}; };
@ -71,23 +73,37 @@ function setStatus(message = '', kind = 'info') {
} }
function makePendingApprovalSnapshot(payload = {}) { function makePendingApprovalSnapshot(payload = {}) {
const pendingId = String(payload?.id || '').trim();
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId);
const queueLength = pendingApprovals.length;
return { return {
id: String(payload?.id || '').trim(), id: pendingId,
kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction', kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction',
origin: String(payload?.origin || '').trim(), origin: String(payload?.origin || '').trim(),
publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(), publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(),
comment: String(payload?.comment || '').trim(), comment: String(payload?.comment || '').trim(),
createdAtMs: Number(payload?.createdAtMs || Date.now()), createdAtMs: Number(payload?.createdAtMs || Date.now()),
status: String(payload?.status || 'queued').trim() || 'queued',
queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1,
queueLength: queueLength || 1,
transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object' transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object'
? { ...payload.transactionSummary } ? { ...payload.transactionSummary }
: null, : null,
}; };
} }
function clearPendingApproval({ rejectError = null } = {}) { function getCurrentPendingApproval() {
if (!state.pendingApproval) return; return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length
const pending = state.pendingApproval; ? state.pendingApprovals[0]
state.pendingApproval = null; : null;
}
function removePendingApproval(pendingId, { rejectError = null } = {}) {
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim());
if (index < 0) return;
const [pending] = pendingApprovals.splice(index, 1);
if (pending.timeoutId) { if (pending.timeoutId) {
clearTimeout(pending.timeoutId); clearTimeout(pending.timeoutId);
} }
@ -139,7 +155,10 @@ async function loadStateFromStorage() {
login: String(settings?.login || '').trim(), login: String(settings?.login || '').trim(),
connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [], connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [],
}; };
state.activeSession = await loadSessionMaterial(); const storedSession = await loadSessionMaterial();
if (storedSession || !state.sessionAttachInProgress) {
state.activeSession = storedSession;
}
state.walletProfile = state.activeSession?.walletProfile || null; state.walletProfile = state.activeSession?.walletProfile || null;
state.signing = { state.signing = {
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''), selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
@ -353,8 +372,17 @@ async function attachApprovedSession(payload) {
throw new Error('Получен неполный session-only payload'); throw new Error('Получен неполный session-only payload');
} }
await clearSessionMaterial(); state.sessionAttachInProgress = true;
try {
state.activeSession = sessionRecord; state.activeSession = sessionRecord;
state.walletProfile = null;
state.currentWallet = null;
state.signing = {
...state.signing,
selectedDeviceName: '',
devicesResolvedAtMs: 0,
};
await saveActiveSessionRecord();
await hydrateWalletProfile(login); await hydrateWalletProfile(login);
await saveActiveSessionRecord(); await saveActiveSessionRecord();
await persistSettings({ await persistSettings({
@ -365,6 +393,9 @@ async function attachApprovedSession(payload) {
}); });
state.connectionOnline = false; state.connectionOnline = false;
state.currentWallet = null; state.currentWallet = null;
} finally {
state.sessionAttachInProgress = false;
}
} }
async function pollPairingStatus() { async function pollPairingStatus() {
@ -666,41 +697,56 @@ async function requestCurrentWallet() {
} }
async function cancelPendingSiteApproval() { async function cancelPendingSiteApproval() {
clearPendingApproval({ const pending = getCurrentPendingApproval();
if (!pending) {
setStatus('Сейчас нет активного ожидания подписи.', 'info');
return { ok: true };
}
removePendingApproval(pending.id, {
rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'), rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'),
}); });
setStatus('Ожидание подписи отменено в расширении.', 'info'); setStatus('Ожидание подписи отменено в расширении.', 'info');
return { ok: true }; return { ok: true };
} }
async function markPendingSiteApprovalResolved() { async function markPendingSiteApprovalResolved(pendingId) {
clearPendingApproval(); removePendingApproval(pendingId);
} }
async function beginSiteTransactionFlow(payload = {}, sender = null) { function enqueueSiteApproval(work) {
if (state.pendingApproval) { const run = state.siteApprovalChain.then(work, work);
throw makeCodeError('Another signing request is already pending.', 'APPROVAL_ALREADY_PENDING'); state.siteApprovalChain = run.catch(() => {});
return run;
} }
async function activatePendingApproval(pending, sender = null) {
const abortController = new AbortController(); const abortController = new AbortController();
pending.status = 'active';
pending.abortController = abortController;
pending.timeoutId = setTimeout(() => {
removePendingApproval(pending.id, {
rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
});
setStatus('Ожидание подписи истекло в расширении.', 'error');
}, 120000);
setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
await openSidePanelForSender(sender);
return pending;
}
function beginSiteTransactionFlow(payload = {}) {
const pending = makePendingApprovalSnapshot({ const pending = makePendingApprovalSnapshot({
...payload, ...payload,
kind: 'sign_transaction', kind: 'sign_transaction',
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
createdAtMs: Date.now(), createdAtMs: Date.now(),
status: 'queued',
}); });
const timeoutId = setTimeout(() => { state.pendingApprovals.push({
clearPendingApproval({
rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
});
setStatus('Ожидание подписи истекло в расширении.', 'error');
}, 120000);
state.pendingApproval = {
...pending, ...pending,
timeoutId, timeoutId: 0,
abortController, abortController: null,
}; });
setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
await openSidePanelForSender(sender);
return pending; return pending;
} }
@ -747,12 +793,15 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
if (!cleanPub || !cleanTx) { if (!cleanPub || !cleanTx) {
throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST'); throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST');
} }
const pending = await beginSiteTransactionFlow({ const pending = beginSiteTransactionFlow({
origin: normalizedOrigin, origin: normalizedOrigin,
publicKeyBase58: cleanPub, publicKeyBase58: cleanPub,
comment: String(comment || '').trim(), comment: String(comment || '').trim(),
transactionSummary: transactionSummary || null, transactionSummary: transactionSummary || null,
}, sender); });
return enqueueSiteApproval(async () => {
await activatePendingApproval(getCurrentPendingApproval() || pending, sender);
const activePending = getCurrentPendingApproval() || pending;
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`; const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
try { try {
@ -763,7 +812,7 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
publicKeyBase58: cleanPub, publicKeyBase58: cleanPub,
transactionBase64: cleanTx, transactionBase64: cleanTx,
comment: signComment, comment: signComment,
}, 120000, state.pendingApproval?.id === pending.id ? state.pendingApproval.abortController.signal : null); }, 120000, activePending.abortController?.signal || null);
if (!response?.ok) { if (!response?.ok) {
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase(); const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
if (errorCode === 'REJECTED_BY_USER') { if (errorCode === 'REJECTED_BY_USER') {
@ -779,8 +828,9 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
signatureBase58: String(response?.signatureBase58 || '').trim(), signatureBase58: String(response?.signatureBase58 || '').trim(),
}; };
} finally { } finally {
await markPendingSiteApprovalResolved(); await markPendingSiteApprovalResolved(activePending.id);
} }
});
} }
function snapshot() { function snapshot() {
@ -797,7 +847,7 @@ function snapshot() {
connectionOnline: !!state.activeSession, connectionOnline: !!state.activeSession,
walletProfile: state.walletProfile ? { ...state.walletProfile } : null, walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
currentWallet: state.currentWallet ? { ...state.currentWallet } : null, currentWallet: state.currentWallet ? { ...state.currentWallet } : null,
pendingApproval: state.pendingApproval ? makePendingApprovalSnapshot(state.pendingApproval) : null, pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null,
signing: { ...state.signing }, signing: { ...state.signing },
status: { status: {
text: state.statusText, text: state.statusText,

View File

@ -118,6 +118,7 @@ function renderPendingApproval(pendingApproval) {
const details = [ const details = [
{ label: 'Сайт', value: pendingApproval.origin || '—', mono: true }, { label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
{ label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true }, { label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
{ label: 'Очередь', value: `${pendingApproval.queuePosition || 1} из ${pendingApproval.queueLength || 1}` },
{ label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' }, { label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
{ label: 'Тип', value: summary.kind || 'legacy' }, { label: 'Тип', value: summary.kind || 'legacy' },
{ label: 'Инструкций', value: String(summary.instructionCount ?? 0) }, { label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
@ -186,9 +187,12 @@ function applyState(nextState) {
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName; els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
if (pendingApproval) { if (pendingApproval) {
const queueSuffix = (pendingApproval.queueLength || 1) > 1
? ` В очереди ${pendingApproval.queueLength} транзакции.`
: '';
els.pendingApprovalSubtitle.textContent = pendingApproval.origin els.pendingApprovalSubtitle.textContent = pendingApproval.origin
? `Сайт ${pendingApproval.origin} запросил подписание транзакции.` ? `Сайт ${pendingApproval.origin} запросил подписание транзакции.${queueSuffix}`
: 'Сайт запросил подписание транзакции.'; : `Сайт запросил подписание транзакции.${queueSuffix}`;
renderPendingApproval(pendingApproval); renderPendingApproval(pendingApproval);
} else { } else {
els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.'; els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';

View File

@ -308,6 +308,15 @@ class ShineSolanaProvider {
return this.core.signTransaction(transaction); return this.core.signTransaction(transaction);
} }
async signAllTransactions(transactions = []) {
const list = Array.isArray(transactions) ? transactions : [];
const outputs = [];
for (const transaction of list) {
outputs.push(await this.core.signTransaction(transaction));
}
return outputs;
}
async request(args = {}) { async request(args = {}) {
const method = String(args?.method || ''); const method = String(args?.method || '');
const params = args?.params; const params = args?.params;
@ -321,6 +330,12 @@ class ShineSolanaProvider {
const tx = Array.isArray(params) ? params[0] : params?.transaction || params; const tx = Array.isArray(params) ? params[0] : params?.transaction || params;
return this.signTransaction(tx); return this.signTransaction(tx);
} }
if (method === 'signAllTransactions') {
const transactions = Array.isArray(params)
? params
: Array.isArray(params?.transactions) ? params.transactions : [];
return this.signAllTransactions(transactions);
}
throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD'); throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD');
} }
} }

View File

@ -1,2 +1,2 @@
client.version=1.2.259 client.version=1.2.260
server.version=1.2.244 server.version=1.2.245

View File

@ -5,7 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<base href="/" /> <base href="/" />
<link rel="manifest" href="./manifest.webmanifest" /> <link rel="manifest" href="./manifest.webmanifest" />
<title>Shine UI Demo</title> <link rel="icon" type="image/jpeg" href="./img/logo.jpg" />
<link rel="apple-touch-icon" href="./img/logo.jpg" />
<title>СИЯНИЕ</title>
<script> <script>
window.__SHINE_BUILD_HASH__ = '20260616130000'; window.__SHINE_BUILD_HASH__ = '20260616130000';
window.__SHINE_CLIENT_VERSION__ = '1.2.8'; window.__SHINE_CLIENT_VERSION__ = '1.2.8';

View File

@ -14,7 +14,7 @@ import {
PASSWORD_WORDS_COUNT, PASSWORD_WORDS_COUNT,
} from '../services/password-words.js'; } from '../services/password-words.js';
import { openRegistrationFaq } from './registration-faq-view.js'; import { openRegistrationFaq } from './registration-faq-view.js';
import { defaultServerHttp, defaultServerLogin } from '../deploy-config.js'; import { defaultServerHttp } from '../deploy-config.js';
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
@ -125,9 +125,7 @@ export function render({ navigate }) {
serverNotice.className = 'card stack'; serverNotice.className = 'card stack';
serverNotice.innerHTML = ` serverNotice.innerHTML = `
<p class="field-label">Первый сервер SHiNE</p> <p class="field-label">Первый сервер SHiNE</p>
<p class="meta-muted">Сейчас вашим первым и основным сервером будет серверный аккаунт <strong>${state.entrySettings.shineServerLogin || defaultServerLogin}</strong>.</p> <p class="meta-muted">При регистрации адресом вашего первого сервера будет: <strong>${state.entrySettings.shineServerHttp || defaultServerHttp}</strong>.</p>
<p class="meta-muted">Текущий адрес этого сервера: <strong>${state.entrySettings.shineServerHttp || defaultServerHttp}</strong>.</p>
<p class="meta-muted">Это первый сервер, на который вам будут писать и звонить после регистрации. Позже его можно будет сменить, а в будущем использовать и несколько серверов сразу.</p>
`; `;
const faqCard = document.createElement('div'); const faqCard = document.createElement('div');
@ -351,10 +349,12 @@ export function render({ navigate }) {
state.registrationDraft.preGeneratedKeyBundle = null; state.registrationDraft.preGeneratedKeyBundle = null;
} }
renderSecurityConfirmStage(); startGenerationStage();
}); });
function renderInputStage() { function renderInputStage() {
serverNotice.style.display = '';
faqCard.style.display = '';
form.innerHTML = ` form.innerHTML = `
<label class="stack"><span class="field-label">Логин</span></label> <label class="stack"><span class="field-label">Логин</span></label>
<label class="stack registration-password-single"><span class="field-label">Пароль</span></label> <label class="stack registration-password-single"><span class="field-label">Пароль</span></label>
@ -363,19 +363,26 @@ export function render({ navigate }) {
passwordField = form.children[1]; passwordField = form.children[1];
loginField.append(loginInput); loginField.append(loginInput);
passwordField.append(passwordInput); passwordField.append(passwordInput);
form.append(passwordModeToggle, wordsSection, passwordLengthText, serverNotice, checkButton, statusText, faqCard, formError); form.append(passwordModeToggle, passwordLengthText, wordsSection, statusText, checkButton, formError);
actions.innerHTML = ''; actions.innerHTML = '';
actions.append(backButton, nextButton); actions.append(backButton, nextButton);
updatePasswordModeVisibility(); updatePasswordModeVisibility();
syncDraftState(); syncDraftState();
} }
function renderSecurityConfirmStage() { async function startGenerationStage() {
serverNotice.style.display = 'none';
faqCard.style.display = 'none';
const runId = ++generationRunId;
form.innerHTML = ''; form.innerHTML = '';
const info = document.createElement('p'); const title = document.createElement('p');
info.className = 'auth-copy'; title.className = 'auth-copy';
info.textContent = 'Для повышения безопасности мы генерируем секрет из вашего логина и пароля с помощью Argon2id.'; title.textContent = 'Для повышения безопасности мы генерируем секрет из вашего логина и пароля с помощью Argon2id.';
const subtitle = document.createElement('p');
subtitle.className = 'meta-muted';
subtitle.textContent = 'Процесс запускается сразу: из этого секрета будут вычислены recovery key, root key, blockchain key и client key.';
const details = document.createElement('p'); const details = document.createElement('p');
details.className = 'meta-muted'; details.className = 'meta-muted';
@ -383,45 +390,11 @@ export function render({ navigate }) {
const details2 = document.createElement('p'); const details2 = document.createElement('p');
details2.className = 'meta-muted'; details2.className = 'meta-muted';
details2.textContent = 'Из этого секрета строятся recovery key, root key, blockchain key и client key. Это может занять некоторое время.'; details2.textContent = 'Замедление нужно специально: оно усложняет подбор пароля и повышает цену атак на видеокартах и GPU.';
const details3 = document.createElement('p'); const details3 = document.createElement('p');
details3.className = 'meta-muted'; details3.className = 'meta-muted';
details3.textContent = 'Замедление нужно специально: оно усложняет подбор пароля и повышает цену атак на видеокартах и GPU.'; details3.textContent = `Длина вашего текущего пароля: ${getCurrentPassword().length} символов.`;
const details4 = document.createElement('p');
details4.className = 'meta-muted';
details4.textContent = `Длина вашего текущего пароля: ${getCurrentPassword().length} символов.`;
form.append(info, details, details2, details3, details4);
const back2 = document.createElement('button');
back2.className = 'ghost-btn';
back2.type = 'button';
back2.textContent = 'Назад';
back2.addEventListener('click', renderInputStage);
const ok = document.createElement('button');
ok.className = 'primary-btn';
ok.type = 'button';
ok.textContent = 'Окей';
ok.addEventListener('click', startGenerationStage);
actions.innerHTML = '';
actions.append(back2, ok);
}
async function startGenerationStage() {
const runId = ++generationRunId;
form.innerHTML = '';
const title = document.createElement('p');
title.className = 'auth-copy';
title.textContent = 'Генерация ключей...';
const subtitle = document.createElement('p');
subtitle.className = 'meta-muted';
subtitle.textContent = 'Генерируется секрет из вашего пароля, из которого будут вычислены все ключи.';
const progressWrap = document.createElement('div'); const progressWrap = document.createElement('div');
progressWrap.className = 'registration-progress'; progressWrap.className = 'registration-progress';
@ -438,7 +411,7 @@ export function render({ navigate }) {
genError.className = 'status-line is-unavailable'; genError.className = 'status-line is-unavailable';
genError.style.display = 'none'; genError.style.display = 'none';
form.append(title, subtitle, progressWrap, progressText, genError); form.append(title, subtitle, details, details2, details3, progressWrap, progressText, genError);
const cancelBtn = document.createElement('button'); const cancelBtn = document.createElement('button');
cancelBtn.className = 'ghost-btn'; cancelBtn.className = 'ghost-btn';
@ -446,7 +419,7 @@ export function render({ navigate }) {
cancelBtn.textContent = 'Отмена'; cancelBtn.textContent = 'Отмена';
cancelBtn.addEventListener('click', () => { cancelBtn.addEventListener('click', () => {
generationRunId += 1; generationRunId += 1;
renderSecurityConfirmStage(); renderInputStage();
}); });
actions.innerHTML = ''; actions.innerHTML = '';
actions.append(cancelBtn); actions.append(cancelBtn);
@ -477,7 +450,7 @@ export function render({ navigate }) {
} catch (error) { } catch (error) {
if (runId !== generationRunId) return; if (runId !== generationRunId) return;
if (String(error?.message || '') === 'DERIVE_CANCELLED') { if (String(error?.message || '') === 'DERIVE_CANCELLED') {
renderSecurityConfirmStage(); renderInputStage();
return; return;
} }
genError.textContent = `Ошибка генерации ключей: ${error?.message || 'неизвестная ошибка'}`; genError.textContent = `Ошибка генерации ключей: ${error?.message || 'неизвестная ошибка'}`;
@ -491,7 +464,7 @@ export function render({ navigate }) {
goBack.className = 'ghost-btn'; goBack.className = 'ghost-btn';
goBack.type = 'button'; goBack.type = 'button';
goBack.textContent = 'Назад'; goBack.textContent = 'Назад';
goBack.addEventListener('click', renderSecurityConfirmStage); goBack.addEventListener('click', renderInputStage);
actions.innerHTML = ''; actions.innerHTML = '';
actions.append(goBack, retry); actions.append(goBack, retry);
} }
@ -505,6 +478,8 @@ export function render({ navigate }) {
leftAction: { label: '←', onClick: () => navigate('start-view') }, leftAction: { label: '←', onClick: () => navigate('start-view') },
}), }),
form, form,
serverNotice,
faqCard,
actions, actions,
); );

View File

@ -1,10 +1,13 @@
import { renderHeader } from '../components/header.js'; import { renderHeader } from '../components/header.js';
import { import {
authService, authService,
authorizeSession,
refreshSessions,
setAuthError, setAuthError,
setAuthInfo, setAuthInfo,
state, state,
} from '../state.js'; } from '../state.js';
import { clearStoredMessages } from '../services/message-store.js';
import { toUserMessage } from '../services/ui-error-texts.js'; import { toUserMessage } from '../services/ui-error-texts.js';
import { import {
formatSol, formatSol,
@ -20,7 +23,8 @@ import { defaultServerLogin } from '../deploy-config.js';
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
const MIN_REQUIRED_SOL = 0.01; const MIN_REQUIRED_SOL = 0.01;
const SOLANA_SYNC_WAIT_SEC = 15; const SOLANA_SYNC_WAIT_SEC = 12;
const EMPTY_PASSWORD_WORDS = Array.from({ length: 12 }, () => '');
function parseBalanceSol(value) { function parseBalanceSol(value) {
const parsed = Number.parseFloat(String(value || '').replace(',', '.')); const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
@ -35,6 +39,69 @@ function getCryptoRuntimeState() {
return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext }; return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext };
} }
async function completeRegistrationLogin({ navigate, keyBundle }) {
await authService.reconnect(state.entrySettings.shineServer);
const result = await authService.createSessionForExistingUser(
state.registrationDraft.login,
state.registrationDraft.password,
);
state.keyStorage.saveRoot = Boolean(state.keyStorage.saveRoot);
state.keyStorage.saveBlockchain = Boolean(state.keyStorage.saveBlockchain);
await authService.persistSelectedKeys(
result.login,
result.storagePwd,
keyBundle,
{
saveRoot: state.keyStorage.saveRoot,
saveBlockchain: state.keyStorage.saveBlockchain,
},
);
await authService.persistSessionMaterial(result.login, result.sessionMaterial);
await clearStoredMessages().catch(() => {});
const resumed = await authService.resumeSession(result.login, result.sessionId);
const resumedLogin = resumed.login || result.login;
const resumedSessionId = resumed.sessionId || result.sessionId;
const resumedStoragePwd = resumed.storagePwd || result.storagePwd;
authorizeSession({
login: resumedLogin,
sessionId: resumedSessionId,
storagePwd: resumedStoragePwd,
});
state.loginDraft.login = resumedLogin;
state.loginDraft.password = '';
state.loginDraft.passwordMode = 'single';
state.loginDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
state.registrationDraft.flowType = '';
state.registrationDraft.password = '';
state.registrationDraft.passwordMode = 'single';
state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
state.registrationDraft.storagePwd = '';
state.registrationDraft.sessionId = '';
state.registrationDraft.pendingKeyBundle = null;
state.registrationDraft.pendingSessionMaterial = null;
state.registrationDraft.preGeneratedKeyBundle = null;
state.registrationPayment.walletAddress = '';
state.registrationPayment.balanceSOL = '0.0000';
await refreshSessions();
setAuthInfo(`Регистрация завершена. Вы автоматически вошли как @${resumedLogin}.`);
const nextHash = String(state.authReturnHash || '').trim();
state.authReturnHash = '';
if (nextHash.startsWith('/')) {
navigate(nextHash.slice(1));
} else {
navigate('profile-view');
}
}
export function render({ navigate }) { export function render({ navigate }) {
const screen = document.createElement('section'); const screen = document.createElement('section');
screen.className = 'stack'; screen.className = 'stack';
@ -258,32 +325,30 @@ function renderSolanaDoneStage({ navigate, status, keyBundle }) {
if (!card) return; if (!card) return;
let remainingSec = SOLANA_SYNC_WAIT_SEC; let remainingSec = SOLANA_SYNC_WAIT_SEC;
let canTryLogin = false;
let timerId = null; let timerId = null;
let loginStarted = false;
const info = document.createElement('p'); const info = document.createElement('p');
info.className = 'auth-copy'; info.className = 'auth-copy';
info.textContent = 'Регистрация в Solana прошла успешно.'; info.textContent = 'Регистрация завершена успешно.';
const hint = document.createElement('p'); const hint = document.createElement('p');
hint.className = 'meta-muted'; hint.className = 'meta-muted';
hint.textContent = 'Подождите 1015 секунд, пока запись обновится в блокчейне. После этого можно входить на сервер.'; hint.textContent = 'Нужно подождать 1015 секунд, пока в блокчейне SHiNE обновится ваша запись о регистрации. После этого мы автоматически войдём в ваш аккаунт.';
const timer = document.createElement('p'); const timer = document.createElement('p');
timer.className = 'meta-muted'; timer.className = 'meta-muted';
timer.textContent = `До попытки входа: ${remainingSec} сек`; timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
const tryLoginBtn = document.createElement('button'); const tryLoginBtn = document.createElement('button');
tryLoginBtn.className = 'primary-btn'; tryLoginBtn.className = 'primary-btn';
tryLoginBtn.type = 'button'; tryLoginBtn.type = 'button';
tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`; tryLoginBtn.textContent = 'Входим автоматически...';
tryLoginBtn.disabled = true; tryLoginBtn.disabled = true;
const backBtn = document.createElement('button'); const statusHint = document.createElement('p');
backBtn.className = 'ghost-btn'; statusHint.className = 'meta-muted';
backBtn.type = 'button'; statusHint.textContent = 'Ничего нажимать не нужно. Если сервер ответит раньше, войдём сразу.';
backBtn.textContent = 'Назад';
backBtn.addEventListener('click', () => navigate('register-view'));
const stopTimer = () => { const stopTimer = () => {
if (timerId) { if (timerId) {
@ -292,54 +357,46 @@ function renderSolanaDoneStage({ navigate, status, keyBundle }) {
} }
}; };
const startAutoLogin = async () => {
if (loginStarted) return;
loginStarted = true;
stopTimer();
status.style.display = 'none';
timer.textContent = 'Пробуем автоматически войти...';
tryLoginBtn.textContent = 'Входим...';
try {
await completeRegistrationLogin({ navigate, keyBundle });
} catch (error) {
loginStarted = false;
status.className = 'status-line is-unavailable';
status.textContent = toUserMessage(error, 'Пока не удалось автоматически войти. Попробуйте ещё раз через несколько секунд.');
status.style.display = '';
timer.textContent = 'Автовход пока не удался.';
tryLoginBtn.disabled = false;
tryLoginBtn.textContent = 'Попробовать войти сейчас';
}
};
const updateTimerUi = () => { const updateTimerUi = () => {
if (remainingSec > 0) { if (remainingSec > 0) {
timer.textContent = `До попытки входа: ${remainingSec} сек`; timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`; tryLoginBtn.textContent = `Автовход через ${remainingSec} сек`;
tryLoginBtn.disabled = true; tryLoginBtn.disabled = true;
} else { } else {
canTryLogin = true; timer.textContent = 'Запускаем автоматический вход...';
timer.textContent = 'Можно входить на сервер.'; tryLoginBtn.textContent = 'Входим...';
tryLoginBtn.textContent = 'Попробовать войти на сервер'; tryLoginBtn.disabled = true;
tryLoginBtn.disabled = false; void startAutoLogin();
stopTimer();
} }
}; };
tryLoginBtn.addEventListener('click', async () => { tryLoginBtn.addEventListener('click', async () => {
if (!canTryLogin) return; if (loginStarted) return;
status.style.display = 'none'; await startAutoLogin();
tryLoginBtn.disabled = true;
tryLoginBtn.textContent = 'Вход...';
try {
await authService.reconnect(state.entrySettings.shineServer);
const result = await authService.createSessionForExistingUser(
state.registrationDraft.login,
state.registrationDraft.password,
);
await authService.persistSessionMaterial(
result.login,
result.sessionMaterial,
);
const resumed = await authService.resumeSession(result.login, result.sessionId);
state.registrationDraft.flowType = 'registration';
state.registrationDraft.sessionId = resumed.sessionId || result.sessionId;
state.registrationDraft.storagePwd = resumed.storagePwd || result.storagePwd;
state.registrationDraft.pendingKeyBundle = keyBundle;
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`);
navigate('registration-keys-view');
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = toUserMessage(error, 'Пока не удалось войти. Попробуйте ещё раз через несколько секунд.');
status.style.display = '';
tryLoginBtn.disabled = false;
tryLoginBtn.textContent = 'Попробовать войти на сервер';
}
}); });
card.innerHTML = ''; card.innerHTML = '';
card.append(info, hint, timer, tryLoginBtn, backBtn, status); card.append(info, hint, statusHint, timer, tryLoginBtn, status);
updateTimerUi(); updateTimerUi();
timerId = window.setInterval(() => { timerId = window.setInterval(() => {
remainingSec -= 1; remainingSec -= 1;

View File

@ -1,6 +1,6 @@
{ {
"name": "Shine UI", "name": "СИЯНИЕ",
"short_name": "Shine", "short_name": "СИЯНИЕ",
"start_url": "./index.html", "start_url": "./index.html",
"display": "fullscreen", "display": "fullscreen",
"display_override": [ "display_override": [