Синхронизировать изменения проекта

This commit is contained in:
AidarKC
2026-07-05 18:18:03 +04:00
parent 0240db59ee
commit 81bef1e1cc
36 changed files with 573 additions and 335 deletions
+92 -21
View File
@@ -13,10 +13,39 @@ import {
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';
import { defaultServerHttp } from '../deploy-config.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;
}
function normalizeLoginForTemporaryUiGuard(login) {
const source = String(login || '').trim();
if (!source || source.length > 20) return null;
let normalized = '';
for (const ch of source) {
if (ch === '_') continue;
if (!/[0-9A-Za-z]/.test(ch)) return null;
normalized += ch.toLowerCase();
}
if (!normalized || normalized.length > 20) return null;
return normalized;
}
function createWordsLayout({ words, onInput }) {
const section = document.createElement('div');
@@ -148,7 +177,7 @@ export function render({ navigate }) {
const promoHint = document.createElement('p');
promoHint.className = 'meta-muted';
promoHint.textContent = 'Если промокод включён, обычная проверка premium/trademark логина пропускается.';
promoHint.textContent = 'Временный режим: логины длиной 5-7 символов доступны только по специальному коду. Любое другое значение считается неверным промокодом.';
promoField.append(promoFieldLabel, promoInput, promoHint);
@@ -247,26 +276,58 @@ export function render({ navigate }) {
async function runAvailabilityCheck() {
const login = loginInput.value.trim();
const promoCode = String(promoInput.value || '').trim();
if (!login) {
statusText.textContent = 'Введите логин';
formError.style.display = 'none';
return false;
}
if (usePromoCode) {
lastCheckedLogin = login;
lastCheckedFree = true;
lastCheckedClassName = 'promo';
statusText.textContent = 'Промокод включён: обычная проверка premium/trademark логина пропущена';
statusText.className = 'status-line is-available';
const normalizedLogin = normalizeLoginForTemporaryUiGuard(login);
if (!normalizedLogin) {
statusText.textContent = 'Логин содержит недопустимые символы или имеет неверную длину ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return true;
return false;
}
if (normalizedLogin.length < TEMP_ABSOLUTE_MIN_LOGIN_LEN) {
statusText.textContent = `Логин должен быть не короче ${TEMP_ABSOLUTE_MIN_LOGIN_LEN} символов ❌`;
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
const wantsShortLogin = normalizedLogin.length < TEMP_MIN_LOGIN_WITHOUT_PROMO;
let promoAccepted = false;
if (usePromoCode) {
if (!promoCode) {
statusText.textContent = 'Введите промокод ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
promoAccepted = await isTemporaryPromoAccepted(promoCode);
if (!promoAccepted) {
statusText.textContent = 'Неверный промокод ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
} else if (wantsShortLogin) {
statusText.textContent = `Логины короче ${TEMP_MIN_LOGIN_WITHOUT_PROMO} символов временно доступны только по специальному коду ❌`;
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
if (login === lastCheckedLogin) {
if (!lastCheckedFree) {
statusText.textContent = 'Логин уже занят ❌';
statusText.className = 'status-line is-unavailable';
} else if (lastCheckedClassName === 'promo') {
statusText.textContent = 'Логин свободен ✅ Временный код принят';
statusText.className = 'status-line is-available';
} else if (lastCheckedClassName === 'free') {
statusText.textContent = 'Логин свободен ✅';
statusText.className = 'status-line is-available';
@@ -281,7 +342,7 @@ export function render({ navigate }) {
statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return lastCheckedFree && lastCheckedClassName === 'free';
return lastCheckedFree && (lastCheckedClassName === 'free' || lastCheckedClassName === 'promo');
}
checkButton.disabled = true;
@@ -295,15 +356,19 @@ export function render({ navigate }) {
let className = '';
let precheckWarning = '';
if (isFree) {
try {
const precheck = await precheckLoginClassOnSolana({
login,
solanaEndpoint: state.entrySettings.solanaServer,
});
className = precheck.className;
} catch (precheckError) {
className = 'free';
precheckWarning = formatSolanaErrorDetails(precheckError);
if (promoAccepted) {
className = 'promo';
} else {
try {
const precheck = await precheckLoginClassOnSolana({
login,
solanaEndpoint: state.entrySettings.solanaServer,
});
className = precheck.className;
} catch (precheckError) {
className = 'free';
precheckWarning = formatSolanaErrorDetails(precheckError);
}
}
}
lastCheckedLogin = login;
@@ -312,6 +377,9 @@ export function render({ navigate }) {
if (!isFree) {
statusText.textContent = 'Логин уже занят ❌';
statusText.className = 'status-line is-unavailable';
} else if (className === 'promo') {
statusText.textContent = 'Логин свободен ✅ Временный код принят';
statusText.className = 'status-line is-available';
} else if (className === 'free') {
statusText.textContent = precheckWarning
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
@@ -328,7 +396,7 @@ export function render({ navigate }) {
statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return isFree && className === 'free';
return isFree && (className === 'free' || className === 'promo');
} catch (error) {
const base = toUserMessage(error, 'Не удалось проверить логин');
const details = formatSolanaErrorDetails(error);
@@ -379,7 +447,7 @@ export function render({ navigate }) {
updatePromoVisibility();
syncDraftState();
if (usePromoCode) {
statusText.textContent = 'Промокод включён: проверка логина будет пропущена';
statusText.textContent = 'Временный код включён: для логинов 5-7 символов будет локальная проверка';
statusText.className = 'status-line is-available';
} else {
statusText.textContent = 'Проверка логина: не выполнена';
@@ -388,6 +456,9 @@ export function render({ navigate }) {
});
promoInput.addEventListener('input', () => {
lastCheckedLogin = '';
lastCheckedFree = false;
lastCheckedClassName = '';
syncDraftState();
});
@@ -395,7 +466,7 @@ export function render({ navigate }) {
formError.style.display = 'none';
const promoCode = String(promoInput.value || '').trim();
if (usePromoCode && !promoCode) {
formError.textContent = 'Если включён промокод, поле промокода должно быть заполнено.';
formError.textContent = 'Если включён временный код, поле должно быть заполнено.';
formError.style.display = '';
return;
}
@@ -266,7 +266,7 @@ export function render({ navigate }) {
keyBundle,
solanaEndpoint: state.entrySettings.solanaServer,
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
promoCode: state.registrationDraft.usePromoCode ? state.registrationDraft.promoCode : '',
promoCode: '',
});
} catch (solanaError) {
const solanaMsg = formatSolanaErrorDetails(solanaError);