Обновить 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
+3 -1
View File
@@ -5,7 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<base href="/" />
<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>
window.__SHINE_BUILD_HASH__ = '20260616130000';
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
+25 -50
View File
@@ -14,7 +14,7 @@ import {
PASSWORD_WORDS_COUNT,
} from '../services/password-words.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 };
@@ -125,9 +125,7 @@ export function render({ navigate }) {
serverNotice.className = 'card stack';
serverNotice.innerHTML = `
<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">Это первый сервер, на который вам будут писать и звонить после регистрации. Позже его можно будет сменить, а в будущем использовать и несколько серверов сразу.</p>
<p class="meta-muted">При регистрации адресом вашего первого сервера будет: <strong>${state.entrySettings.shineServerHttp || defaultServerHttp}</strong>.</p>
`;
const faqCard = document.createElement('div');
@@ -351,10 +349,12 @@ export function render({ navigate }) {
state.registrationDraft.preGeneratedKeyBundle = null;
}
renderSecurityConfirmStage();
startGenerationStage();
});
function renderInputStage() {
serverNotice.style.display = '';
faqCard.style.display = '';
form.innerHTML = `
<label class="stack"><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];
loginField.append(loginInput);
passwordField.append(passwordInput);
form.append(passwordModeToggle, wordsSection, passwordLengthText, serverNotice, checkButton, statusText, faqCard, formError);
form.append(passwordModeToggle, passwordLengthText, wordsSection, statusText, checkButton, formError);
actions.innerHTML = '';
actions.append(backButton, nextButton);
updatePasswordModeVisibility();
syncDraftState();
}
function renderSecurityConfirmStage() {
async function startGenerationStage() {
serverNotice.style.display = 'none';
faqCard.style.display = 'none';
const runId = ++generationRunId;
form.innerHTML = '';
const info = document.createElement('p');
info.className = 'auth-copy';
info.textContent = 'Для повышения безопасности мы генерируем секрет из вашего логина и пароля с помощью Argon2id.';
const title = document.createElement('p');
title.className = 'auth-copy';
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');
details.className = 'meta-muted';
@@ -383,45 +390,11 @@ export function render({ navigate }) {
const details2 = document.createElement('p');
details2.className = 'meta-muted';
details2.textContent = 'Из этого секрета строятся recovery key, root key, blockchain key и client key. Это может занять некоторое время.';
details2.textContent = 'Замедление нужно специально: оно усложняет подбор пароля и повышает цену атак на видеокартах и GPU.';
const details3 = document.createElement('p');
details3.className = 'meta-muted';
details3.textContent = 'Замедление нужно специально: оно усложняет подбор пароля и повышает цену атак на видеокартах и GPU.';
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 = 'Генерируется секрет из вашего пароля, из которого будут вычислены все ключи.';
details3.textContent = `Длина вашего текущего пароля: ${getCurrentPassword().length} символов.`;
const progressWrap = document.createElement('div');
progressWrap.className = 'registration-progress';
@@ -438,7 +411,7 @@ export function render({ navigate }) {
genError.className = 'status-line is-unavailable';
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');
cancelBtn.className = 'ghost-btn';
@@ -446,7 +419,7 @@ export function render({ navigate }) {
cancelBtn.textContent = 'Отмена';
cancelBtn.addEventListener('click', () => {
generationRunId += 1;
renderSecurityConfirmStage();
renderInputStage();
});
actions.innerHTML = '';
actions.append(cancelBtn);
@@ -477,7 +450,7 @@ export function render({ navigate }) {
} catch (error) {
if (runId !== generationRunId) return;
if (String(error?.message || '') === 'DERIVE_CANCELLED') {
renderSecurityConfirmStage();
renderInputStage();
return;
}
genError.textContent = `Ошибка генерации ключей: ${error?.message || 'неизвестная ошибка'}`;
@@ -491,7 +464,7 @@ export function render({ navigate }) {
goBack.className = 'ghost-btn';
goBack.type = 'button';
goBack.textContent = 'Назад';
goBack.addEventListener('click', renderSecurityConfirmStage);
goBack.addEventListener('click', renderInputStage);
actions.innerHTML = '';
actions.append(goBack, retry);
}
@@ -505,6 +478,8 @@ export function render({ navigate }) {
leftAction: { label: '←', onClick: () => navigate('start-view') },
}),
form,
serverNotice,
faqCard,
actions,
);
+105 -48
View File
@@ -1,10 +1,13 @@
import { renderHeader } from '../components/header.js';
import {
authService,
authorizeSession,
refreshSessions,
setAuthError,
setAuthInfo,
state,
} from '../state.js';
import { clearStoredMessages } from '../services/message-store.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import {
formatSol,
@@ -20,7 +23,8 @@ import { defaultServerLogin } from '../deploy-config.js';
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
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) {
const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
@@ -35,6 +39,69 @@ function getCryptoRuntimeState() {
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 }) {
const screen = document.createElement('section');
screen.className = 'stack';
@@ -258,32 +325,30 @@ function renderSolanaDoneStage({ navigate, status, keyBundle }) {
if (!card) return;
let remainingSec = SOLANA_SYNC_WAIT_SEC;
let canTryLogin = false;
let timerId = null;
let loginStarted = false;
const info = document.createElement('p');
info.className = 'auth-copy';
info.textContent = 'Регистрация в Solana прошла успешно.';
info.textContent = 'Регистрация завершена успешно.';
const hint = document.createElement('p');
hint.className = 'meta-muted';
hint.textContent = 'Подождите 10–15 секунд, пока запись обновится в блокчейне. После этого можно входить на сервер.';
hint.textContent = 'Нужно подождать 10–15 секунд, пока в блокчейне SHiNE обновится ваша запись о регистрации. После этого мы автоматически войдём в ваш аккаунт.';
const timer = document.createElement('p');
timer.className = 'meta-muted';
timer.textContent = `До попытки входа: ${remainingSec} сек`;
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
const tryLoginBtn = document.createElement('button');
tryLoginBtn.className = 'primary-btn';
tryLoginBtn.type = 'button';
tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`;
tryLoginBtn.textContent = 'Входим автоматически...';
tryLoginBtn.disabled = true;
const backBtn = document.createElement('button');
backBtn.className = 'ghost-btn';
backBtn.type = 'button';
backBtn.textContent = 'Назад';
backBtn.addEventListener('click', () => navigate('register-view'));
const statusHint = document.createElement('p');
statusHint.className = 'meta-muted';
statusHint.textContent = 'Ничего нажимать не нужно. Если сервер ответит раньше, войдём сразу.';
const stopTimer = () => {
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 = () => {
if (remainingSec > 0) {
timer.textContent = `До попытки входа: ${remainingSec} сек`;
tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`;
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
tryLoginBtn.textContent = `Автовход через ${remainingSec} сек`;
tryLoginBtn.disabled = true;
} else {
canTryLogin = true;
timer.textContent = 'Можно входить на сервер.';
tryLoginBtn.textContent = 'Попробовать войти на сервер';
tryLoginBtn.disabled = false;
stopTimer();
timer.textContent = 'Запускаем автоматический вход...';
tryLoginBtn.textContent = 'Входим...';
tryLoginBtn.disabled = true;
void startAutoLogin();
}
};
tryLoginBtn.addEventListener('click', async () => {
if (!canTryLogin) return;
status.style.display = 'none';
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 = 'Попробовать войти на сервер';
}
if (loginStarted) return;
await startAutoLogin();
});
card.innerHTML = '';
card.append(info, hint, timer, tryLoginBtn, backBtn, status);
card.append(info, hint, statusHint, timer, tryLoginBtn, status);
updateTimerUi();
timerId = window.setInterval(() => {
remainingSec -= 1;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Shine UI",
"short_name": "Shine",
"name": "СИЯНИЕ",
"short_name": "СИЯНИЕ",
"start_url": "./index.html",
"display": "fullscreen",
"display_override": [