Обновить UI кошелька и регистрацию
This commit is contained in:
parent
684f3237cf
commit
77f5759d60
@ -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,18 +372,30 @@ async function attachApprovedSession(payload) {
|
|||||||
throw new Error('Получен неполный session-only payload');
|
throw new Error('Получен неполный session-only payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
await clearSessionMaterial();
|
state.sessionAttachInProgress = true;
|
||||||
state.activeSession = sessionRecord;
|
try {
|
||||||
await hydrateWalletProfile(login);
|
state.activeSession = sessionRecord;
|
||||||
await saveActiveSessionRecord();
|
state.walletProfile = null;
|
||||||
await persistSettings({
|
state.currentWallet = null;
|
||||||
login: sessionRecord.login,
|
state.signing = {
|
||||||
serverLogin: sessionRecord.serverLogin,
|
...state.signing,
|
||||||
serverHttp: sessionRecord.serverHttp,
|
selectedDeviceName: '',
|
||||||
serverUrl: sessionRecord.serverUrl,
|
devicesResolvedAtMs: 0,
|
||||||
});
|
};
|
||||||
state.connectionOnline = false;
|
await saveActiveSessionRecord();
|
||||||
state.currentWallet = null;
|
await hydrateWalletProfile(login);
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
await persistSettings({
|
||||||
|
login: sessionRecord.login,
|
||||||
|
serverLogin: sessionRecord.serverLogin,
|
||||||
|
serverHttp: sessionRecord.serverHttp,
|
||||||
|
serverUrl: sessionRecord.serverUrl,
|
||||||
|
});
|
||||||
|
state.connectionOnline = false;
|
||||||
|
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,40 +793,44 @@ 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);
|
});
|
||||||
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
return enqueueSiteApproval(async () => {
|
||||||
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
|
await activatePendingApproval(getCurrentPendingApproval() || pending, sender);
|
||||||
try {
|
const activePending = getCurrentPendingApproval() || pending;
|
||||||
const { response } = await callWalletRpc({
|
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
v: 1,
|
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
|
||||||
operation: 'sign_transaction',
|
try {
|
||||||
requestId,
|
const { response } = await callWalletRpc({
|
||||||
publicKeyBase58: cleanPub,
|
v: 1,
|
||||||
transactionBase64: cleanTx,
|
operation: 'sign_transaction',
|
||||||
comment: signComment,
|
requestId,
|
||||||
}, 120000, state.pendingApproval?.id === pending.id ? state.pendingApproval.abortController.signal : null);
|
publicKeyBase58: cleanPub,
|
||||||
if (!response?.ok) {
|
transactionBase64: cleanTx,
|
||||||
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
|
comment: signComment,
|
||||||
if (errorCode === 'REJECTED_BY_USER') {
|
}, 120000, activePending.abortController?.signal || null);
|
||||||
throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
|
if (!response?.ok) {
|
||||||
|
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
|
||||||
|
if (errorCode === 'REJECTED_BY_USER') {
|
||||||
|
throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
|
||||||
|
}
|
||||||
|
throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
|
||||||
}
|
}
|
||||||
throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
|
setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(),
|
||||||
|
signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(),
|
||||||
|
signatureBase58: String(response?.signatureBase58 || '').trim(),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await markPendingSiteApprovalResolved(activePending.id);
|
||||||
}
|
}
|
||||||
setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info');
|
});
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(),
|
|
||||||
signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(),
|
|
||||||
signatureBase58: String(response?.signatureBase58 || '').trim(),
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
await markPendingSiteApprovalResolved();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@ -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 = 'Сайт запросил подписание транзакции.';
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.259
|
client.version=1.2.260
|
||||||
server.version=1.2.244
|
server.version=1.2.245
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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 = 'Подождите 10–15 секунд, пока запись обновится в блокчейне. После этого можно входить на сервер.';
|
hint.textContent = 'Нужно подождать 10–15 секунд, пока в блокчейне 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;
|
||||||
|
|||||||
@ -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": [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user