diff --git a/VERSION.properties b/VERSION.properties index a6f9ae24..0452f76c 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.0 +client.version=1.5.1 server.version=1.4.5 diff --git a/docs/Figma/TRANSFER_UI_SCREENS.md b/docs/Figma/TRANSFER_UI_SCREENS.md index 59aca872..d680d1c4 100644 --- a/docs/Figma/TRANSFER_UI_SCREENS.md +++ b/docs/Figma/TRANSFER_UI_SCREENS.md @@ -155,8 +155,6 @@ При переносе нужно отдельно учитывать: - заголовок и стрелку назад; - поля логина и пароля; -- переключатель режима 12 слов; -- сетку слов; - строку статуса длины пароля; - строку статуса проверки логина; - кнопку проверки логина; diff --git a/shine-UI/index.html b/shine-UI/index.html index f77f70ad..7a10f904 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -9,7 +9,7 @@ СИЯНИЕ +
+
+ +
+
Сияние
+
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index d8034140..82e141ee 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -152,6 +152,7 @@ const routes = { const screenEl = document.getElementById('app-screen'); const toolbarEl = document.getElementById('toolbar-slot'); const appShellEl = document.querySelector('.app-shell'); +const initialSplashEl = document.getElementById('initial-splash'); const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000; const SIGNED_DM_DECRYPT_CONTEXT_WAIT_MS = 2000; @@ -177,6 +178,7 @@ let uiVersionCheckInFlight = false; let uiVersionPeriodicIntervalId = null; let hiddenDmAudioContext = null; let hiddenDmAudioUnlocked = false; +let initialConnectionCompleted = false; const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1'; const GUEST_ALLOWED_PAGES = new Set([ 'start-view', @@ -331,6 +333,11 @@ function refreshConnectionUi() { const bannerEl = ensureConnectionRetryBannerEl(); if (!bannerEl) return; + if (!initialConnectionCompleted) { + bannerEl.hidden = true; + return; + } + if (state === 'connected' || state === 'updating') { bannerEl.hidden = true; stopConnectionCountdown(); @@ -360,6 +367,21 @@ function refreshConnectionUi() { bannerEl.textContent = 'Проблема с соединением. Нажмите для повтора'; } +function finishInitialConnectionSplash() { + if (initialConnectionCompleted) return; + initialConnectionCompleted = true; + if (!initialSplashEl) { + refreshConnectionUi(); + return; + } + + initialSplashEl.classList.add('is-leaving'); + const removeSplash = () => initialSplashEl.remove(); + initialSplashEl.addEventListener('transitionend', removeSplash, { once: true }); + window.setTimeout(removeSplash, 700); + refreshConnectionUi(); +} + function startConnectionCountdown() { if (connectionStatusCountdownId) return; connectionStatusCountdownId = window.setInterval(() => { @@ -467,6 +489,9 @@ function setConnectionStatus(nextState, text = '') { if (!state) return; connectionState = state; connectionStatusText = String(text || '').trim(); + if (state === 'connected') { + finishInitialConnectionSplash(); + } if (state === 'disconnected') { if (!connectionNextRetryAtMs || connectionNextRetryAtMs <= Date.now()) { connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS; diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 437584d4..ff17e99f 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -7,31 +7,13 @@ import { precheckLoginClassOnSolana, } from '../services/solana-register-service.js'; import { - composePasswordFromWords, emptyPasswordWords, - normalizePasswordWords, PASSWORD_MAX_LENGTH, - PASSWORD_WORDS_COUNT, } from '../services/password-words.js'; -import { sha256Text } from '../services/crypto-utils.js'; import { openRegistrationFaq } from './registration-faq-view.js'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; -// ВРЕМЕННАЯ UI-ЗАГЛУШКА: -// пока полноценная логика продажи/выдачи коротких имён не внедрена on-chain, -// UI пускает логины 5..7 символов только по временному коду. -const TEMP_MIN_LOGIN_WITHOUT_PROMO = 8; -const TEMP_ABSOLUTE_MIN_LOGIN_LEN = 5; -const TEMP_PROMO_HASH_HEX = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'; - -function bytesToHex(bytes) { - return Array.from(bytes || [], (b) => b.toString(16).padStart(2, '0')).join(''); -} - -async function isTemporaryPromoAccepted(value) { - const digest = await sha256Text(String(value || '')); - return bytesToHex(digest) === TEMP_PROMO_HASH_HEX; -} +const MIN_REGISTRATION_LOGIN_LENGTH = 8; function normalizeLoginForTemporaryUiGuard(login) { const source = String(login || '').trim(); @@ -46,48 +28,6 @@ function normalizeLoginForTemporaryUiGuard(login) { return normalized; } -function createWordsLayout({ words, onInput }) { - const section = document.createElement('div'); - section.className = 'registration-words-block'; - - const grid = document.createElement('div'); - grid.className = 'registration-words-grid'; - - const inputs = Array.from({ length: PASSWORD_WORDS_COUNT }, (_, index) => { - const row = document.createElement('label'); - row.className = 'registration-word-row'; - - const number = document.createElement('span'); - number.className = 'registration-word-number'; - number.textContent = `${index + 1}.`; - - const input = document.createElement('input'); - input.className = 'input registration-word-input'; - input.type = 'text'; - input.autocomplete = 'off'; - input.autocapitalize = 'off'; - input.spellcheck = false; - input.maxLength = 32; - input.value = words[index]; - input.addEventListener('input', () => onInput(index, input.value)); - - row.append(number, input); - grid.append(row); - return input; - }); - - const hint = document.createElement('p'); - hint.className = 'meta-muted'; - hint.textContent = - 'Здесь можно ввести любые слова на любых языках. Мы не проверяем орфографию. Можно заполнить все 12 полей или только часть. В конце всё склеивается в один пароль длиной до 256 символов.'; - - const preview = document.createElement('p'); - preview.className = 'status-line'; - - section.append(grid, hint); - return { section, inputs, preview }; -} - function makePasswordToggleIcons() { return { eye: ` @@ -115,10 +55,11 @@ export function render({ navigate }) { const form = document.createElement('div'); form.className = 'card stack registration-form'; - let passwordMode = String(state.registrationDraft.passwordMode || 'single') === 'words' ? 'words' : 'single'; - let passwordWords = normalizePasswordWords(state.registrationDraft.passwordWords); - let passwordWordsLinked = Boolean(state.registrationDraft.passwordWordsLinked); - let usePromoCode = Boolean(state.registrationDraft.usePromoCode); + state.registrationDraft.passwordMode = 'single'; + state.registrationDraft.passwordWords = emptyPasswordWords(); + state.registrationDraft.passwordWordsLinked = false; + state.registrationDraft.usePromoCode = false; + state.registrationDraft.promoCode = ''; let loginCheckTimer = 0; let loginCheckRunId = 0; @@ -155,67 +96,6 @@ export function render({ navigate }) { passwordInputRow.className = 'inline-input-row'; passwordInputRow.append(passwordInput, passwordToggleButton); - const { - section: wordsSection, - inputs: wordInputs, - preview: wordsPreview, - } = createWordsLayout({ - words: passwordWords, - onInput: (index, value) => { - passwordWords[index] = value; - passwordInput.value = composePasswordFromWords(passwordWords); - passwordWordsLinked = true; - syncDraftState(); - updateWordsPreview(); - }, - }); - - const passwordModeToggle = document.createElement('label'); - passwordModeToggle.className = 'registration-toggle'; - - const passwordModeCheckbox = document.createElement('input'); - passwordModeCheckbox.type = 'checkbox'; - passwordModeCheckbox.checked = passwordMode === 'words'; - - const passwordModeLabel = document.createElement('span'); - passwordModeLabel.textContent = 'Представить пароль в виде 12 слов'; - - passwordModeToggle.append(passwordModeCheckbox, passwordModeLabel); - - const promoToggle = document.createElement('label'); - promoToggle.className = 'registration-toggle'; - - const promoCheckbox = document.createElement('input'); - promoCheckbox.type = 'checkbox'; - promoCheckbox.checked = usePromoCode; - - const promoToggleLabel = document.createElement('span'); - promoToggleLabel.textContent = 'У меня есть промокод'; - - promoToggle.append(promoCheckbox, promoToggleLabel); - - const promoField = document.createElement('label'); - promoField.className = 'stack'; - - const promoFieldLabel = document.createElement('span'); - promoFieldLabel.className = 'field-label'; - promoFieldLabel.textContent = 'Промокод'; - - const promoInput = document.createElement('input'); - promoInput.className = 'input'; - promoInput.type = 'text'; - promoInput.autocomplete = 'off'; - promoInput.autocapitalize = 'off'; - promoInput.spellcheck = false; - promoInput.value = String(state.registrationDraft.promoCode || ''); - promoInput.placeholder = 'Вставьте промокод'; - - const promoHint = document.createElement('p'); - promoHint.className = 'meta-muted'; - promoHint.textContent = 'Временный режим: логины длиной 5-7 символов доступны только по специальному коду. Любое другое значение считается неверным промокодом.'; - - promoField.append(promoFieldLabel, promoInput, promoHint); - const statusText = document.createElement('p'); statusText.className = 'status-line registration-login-status'; statusText.style.display = 'none'; @@ -239,7 +119,6 @@ export function render({ navigate }) { nextButton.type = 'button'; nextButton.textContent = 'Далее'; - let passwordField = null; const passwordLengthText = document.createElement('p'); passwordLengthText.className = 'password-length-hint'; let lastCheckedLogin = ''; @@ -251,13 +130,8 @@ export function render({ navigate }) { return String(passwordInput.value || ''); } - function updateWordsPreview() { - const password = passwordMode === 'words' - ? composePasswordFromWords(passwordWords) - : String(passwordInput.value || ''); - const text = `Итоговая длина пароля: ${password.length} символов.`; - wordsPreview.textContent = text; - passwordLengthText.textContent = text; + function updatePasswordLength() { + passwordLengthText.textContent = `Итоговая длина пароля: ${getCurrentPassword().length} символов.`; } function setStatusMessage(message, kind = '') { @@ -295,32 +169,14 @@ export function render({ navigate }) { passwordToggleButton.setAttribute('title', reveal ? 'Скрыть пароль' : 'Показать пароль'); } - function updatePasswordModeVisibility() { - const wordsMode = passwordMode === 'words'; - wordsSection.style.display = wordsMode ? 'grid' : 'none'; - if (passwordField) passwordField.style.display = 'grid'; - passwordInputRow.style.display = 'grid'; - updateWordsPreview(); - } - function syncDraftState() { state.registrationDraft.login = String(loginInput.value.trim()); - state.registrationDraft.passwordMode = passwordMode; - state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords); - state.registrationDraft.passwordWordsLinked = passwordWordsLinked; state.registrationDraft.password = getCurrentPassword(); - state.registrationDraft.usePromoCode = usePromoCode; - state.registrationDraft.promoCode = String(promoInput.value || '').trim(); - } - - function updatePromoVisibility() { - promoField.style.display = usePromoCode ? 'grid' : 'none'; } async function runAvailabilityCheck({ automatic = false } = {}) { const runId = ++loginCheckRunId; const login = loginInput.value.trim(); - const promoCode = String(promoInput.value || '').trim(); if (!login) { setStatusMessage(automatic ? '' : 'Введите логин'); formError.style.display = 'none'; @@ -334,28 +190,8 @@ export function render({ navigate }) { return false; } - if (normalizedLogin.length < TEMP_ABSOLUTE_MIN_LOGIN_LEN) { - setStatusMessage(`Логин должен быть не короче ${TEMP_ABSOLUTE_MIN_LOGIN_LEN} символов ❌`, 'is-unavailable'); - formError.style.display = 'none'; - return false; - } - - const wantsShortLogin = normalizedLogin.length < TEMP_MIN_LOGIN_WITHOUT_PROMO; - let promoAccepted = false; - if (usePromoCode) { - if (!promoCode) { - setStatusMessage('Введите промокод ❌', 'is-unavailable'); - formError.style.display = 'none'; - return false; - } - promoAccepted = await isTemporaryPromoAccepted(promoCode); - if (!promoAccepted) { - setStatusMessage('Неверный промокод ❌', 'is-unavailable'); - formError.style.display = 'none'; - return false; - } - } else if (wantsShortLogin) { - setStatusMessage(`Логины короче ${TEMP_MIN_LOGIN_WITHOUT_PROMO} символов временно доступны только по специальному коду ❌`, 'is-unavailable'); + if (normalizedLogin.length < MIN_REGISTRATION_LOGIN_LENGTH) { + setStatusMessage(`Логин должен быть не короче ${MIN_REGISTRATION_LOGIN_LENGTH} символов ❌`, 'is-unavailable'); formError.style.display = 'none'; return false; } @@ -363,8 +199,6 @@ export function render({ navigate }) { if (login === lastCheckedLogin) { if (!lastCheckedFree) { setStatusMessage('Логин уже занят ❌', 'is-unavailable'); - } else if (lastCheckedClassName === 'promo') { - setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available'); } else if (lastCheckedClassName === 'free') { setStatusMessage('Логин свободен ✅', 'is-available'); } else if (lastCheckedClassName === 'premium') { @@ -375,7 +209,7 @@ export function render({ navigate }) { setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable'); } formError.style.display = 'none'; - return lastCheckedFree && (lastCheckedClassName === 'free' || lastCheckedClassName === 'promo'); + return lastCheckedFree && lastCheckedClassName === 'free'; } setStatusMessage('Проверяем логин...'); @@ -388,19 +222,15 @@ export function render({ navigate }) { const isFree = !check.exists; let className = ''; if (isFree) { - if (promoAccepted) { - className = 'promo'; - } else { - try { - const precheck = await precheckLoginClassOnSolana({ - login, - solanaEndpoint: state.entrySettings.solanaServer, - }); - className = precheck.className; - } catch (precheckError) { - className = 'free'; - console.warn('Solana login precheck fallback to free', formatSolanaErrorDetails(precheckError)); - } + try { + const precheck = await precheckLoginClassOnSolana({ + login, + solanaEndpoint: state.entrySettings.solanaServer, + }); + className = precheck.className; + } catch (precheckError) { + className = 'free'; + console.warn('Solana login precheck fallback to free', formatSolanaErrorDetails(precheckError)); } } lastCheckedLogin = login; @@ -408,8 +238,6 @@ export function render({ navigate }) { lastCheckedClassName = className; if (!isFree) { setStatusMessage('Логин уже занят ❌', 'is-unavailable'); - } else if (className === 'promo') { - setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available'); } else if (className === 'free') { setStatusMessage('Логин свободен ✅', 'is-available'); } else if (className === 'premium') { @@ -420,7 +248,7 @@ export function render({ navigate }) { setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable'); } formError.style.display = 'none'; - return isFree && (className === 'free' || className === 'promo'); + return isFree && className === 'free'; } catch (error) { if (runId !== loginCheckRunId) return false; const base = toUserMessage(error, 'Не удалось проверить логин'); @@ -438,65 +266,12 @@ export function render({ navigate }) { }); passwordInput.addEventListener('input', () => { - if (passwordWordsLinked && String(passwordInput.value || '') !== composePasswordFromWords(passwordWords)) { - passwordWords = emptyPasswordWords(); - passwordWordsLinked = false; - wordInputs.forEach((input) => { - input.value = ''; - }); - } syncDraftState(); - updateWordsPreview(); - }); - - passwordModeCheckbox.addEventListener('change', () => { - const nextMode = passwordModeCheckbox.checked ? 'words' : 'single'; - if (nextMode === passwordMode) return; - if (nextMode === 'words') { - if (!passwordWordsLinked) { - passwordWords = emptyPasswordWords(); - wordInputs.forEach((input) => { - input.value = ''; - }); - } else { - passwordInput.value = composePasswordFromWords(passwordWords); - } - } else if (passwordWordsLinked) { - passwordInput.value = composePasswordFromWords(passwordWords); - } - passwordMode = nextMode; - updatePasswordModeVisibility(); - updateWordsPreview(); - syncDraftState(); - }); - - promoCheckbox.addEventListener('change', () => { - usePromoCode = promoCheckbox.checked; - resetLoginCheckState(); - updatePromoVisibility(); - syncDraftState(); - if (usePromoCode) { - setStatusMessage('Временный код включён: для логинов 5-7 символов будет локальная проверка', 'is-available'); - } else { - setStatusMessage(''); - scheduleAvailabilityCheck(); - } - }); - - promoInput.addEventListener('input', () => { - resetLoginCheckState(); - syncDraftState(); - if (usePromoCode) scheduleAvailabilityCheck(); + updatePasswordLength(); }); nextButton.addEventListener('click', async () => { formError.style.display = 'none'; - const promoCode = String(promoInput.value || '').trim(); - if (usePromoCode && !promoCode) { - formError.textContent = 'Если включён временный код, поле должно быть заполнено.'; - formError.style.display = ''; - return; - } const isFree = await runAvailabilityCheck(); if (!isFree) return; @@ -518,11 +293,6 @@ export function render({ navigate }) { state.registrationDraft.login = nextLogin; state.registrationDraft.password = nextPassword; - state.registrationDraft.passwordMode = passwordMode; - state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords); - state.registrationDraft.passwordWordsLinked = passwordWordsLinked; - state.registrationDraft.usePromoCode = usePromoCode; - state.registrationDraft.promoCode = promoCode; if (credsChanged) { state.registrationDraft.preGeneratedKeyBundle = null; } @@ -539,14 +309,12 @@ export function render({ navigate }) { passwordLabel.className = 'stack registration-password-single'; passwordLabel.innerHTML = 'Пароль'; form.append(loginField, statusText, passwordLabel); - passwordField = passwordLabel; loginField.append(loginInput); - passwordField.append(passwordInputRow); - form.append(passwordModeToggle, promoToggle, promoField, passwordLengthText, wordsSection, formError, faqButton); + passwordLabel.append(passwordInputRow); + form.append(passwordLengthText, formError, faqButton); actions.innerHTML = ''; actions.append(nextButton); - updatePasswordModeVisibility(); - updatePromoVisibility(); + updatePasswordLength(); syncDraftState(); } diff --git a/shine-UI/js/pages/registration-faq-view.js b/shine-UI/js/pages/registration-faq-view.js index 078d2e14..72845fbe 100644 --- a/shine-UI/js/pages/registration-faq-view.js +++ b/shine-UI/js/pages/registration-faq-view.js @@ -62,17 +62,7 @@ export const REGISTRATION_FAQ_TOPICS = [ paragraphs: [ 'Минимально разумный уровень сейчас начинается примерно от 8 символов.', 'Хороший практический ориентир для большинства людей: 12 символов и больше. Пароль у нас может быть длиной до 256 символов.', - 'Если вам удобнее думать словами, можно использовать режим из 12 полей ниже: слова просто склеиваются в один длинный пароль, и система не проверяет орфографию.', - ], - }, - { - id: 'one-or-twelve', - shortTitle: '1 или 12 слов', - title: 'Чем отличается один пароль от режима 12 слов?', - paragraphs: [ - 'Технически ничем: это один и тот же пароль. Режим 12 слов нужен только для удобства запоминания и ввода.', - 'Можно заполнить все 12 полей, можно только первые 6, можно использовать слова от другого кошелька, разные языки и любые нестандартные символы.', - 'Главное помнить, что в конце всё равно получается одна строка длиной до 256 символов.', + 'Используйте уникальный пароль, который не применяется в других сервисах.', ], }, { diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 6c2284fc..594907b3 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -438,6 +438,70 @@ text-align: center; } +.initial-splash { + position: fixed; + inset: 0; + z-index: 10000; + display: grid; + align-content: center; + justify-items: center; + gap: clamp(12px, 2.4vh, 20px); + padding: 24px; + overflow: hidden; + background: + radial-gradient(circle at 50% 18%, rgba(224, 172, 75, 0.08), transparent 26%), + radial-gradient(circle at 50% 67%, rgba(45, 81, 143, 0.06), transparent 38%), + linear-gradient(180deg, #040816 0%, #020611 54%, #01040c 100%); + font-family: "Manrope", "Inter", "SF Pro Display", system-ui, sans-serif; + opacity: 1; + visibility: visible; + transition: opacity 520ms ease, visibility 520ms ease; +} + +.initial-splash.is-leaving { + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.initial-splash__logo-wrap { + position: relative; + isolation: isolate; + width: clamp(232px, 59vw, 284px); + aspect-ratio: 1; +} + +.initial-splash__logo-wrap::before { + content: ""; + position: absolute; + inset: 16%; + z-index: -1; + border-radius: 50%; + background: rgba(214, 155, 56, 0.2); + filter: blur(30px); + animation: shine-halo-breathe 4.8s ease-in-out infinite; +} + +.initial-splash__logo { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + filter: + drop-shadow(0 0 8px rgba(255, 211, 121, 0.72)) + drop-shadow(0 8px 22px rgba(225, 158, 48, 0.38)); + animation: shine-logo-breathe 4.8s ease-in-out infinite; +} + +.initial-splash__brand { + margin-top: -4px; + color: #f4ebdd; + font-size: clamp(42px, 9vw, 56px); + font-weight: 500; + line-height: 1; + text-shadow: 0 3px 22px rgba(210, 168, 90, 0.2); +} + .auth-screen--welcome { position: relative; isolation: isolate; @@ -709,6 +773,14 @@ } @media (max-height: 760px) { + .initial-splash__logo-wrap { + width: 206px; + } + + .initial-splash__brand { + font-size: 40px; + } + .auth-screen--welcome { gap: 10px; } @@ -732,6 +804,12 @@ } @media (prefers-reduced-motion: reduce) { + .initial-splash { + transition-duration: 1ms; + } + + .initial-splash__logo, + .initial-splash__logo-wrap::before, .auth-logo, .auth-logo-wrap::before, .shine-btn {