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

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);
+2 -1
View File
@@ -1,5 +1,6 @@
export const SOLANA_CLUSTER = 'mainnet-beta';
export const SOLANA_ENDPOINT_DEFAULT = 'https://api.mainnet-beta.solana.com';
// Для браузерного UI используем endpoint, который не режет JSON-RPC по Origin.
export const SOLANA_ENDPOINT_DEFAULT = 'https://solana-rpc.publicnode.com';
// Единый файл актуальных Solana Program ID для UI-части SHiNE.
// При смене адресов обновлять этот файл и все его потребители внутри shine-UI.
+18 -3
View File
@@ -92,6 +92,21 @@ const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
function normalizeStoredSolanaServer(value) {
const raw = String(value || '').trim();
if (!raw) return DEFAULT_SOLANA_SERVER;
const normalized = raw.replace(/\/+$/u, '').toLowerCase();
if (
normalized === 'https://api.devnet.solana.com'
|| normalized === 'http://api.devnet.solana.com'
|| normalized === 'https://api.mainnet-beta.solana.com'
|| normalized === 'http://api.mainnet-beta.solana.com'
) {
return DEFAULT_SOLANA_SERVER;
}
return raw;
}
export function normalizeDmChatId(value) {
return String(value || '').trim().toLowerCase();
}
@@ -182,7 +197,7 @@ function persistEntrySettings(settings) {
try {
const payload = {
language: String(settings?.language || 'ru'),
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(settings?.solanaServer),
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
shineServerLogin: String(settings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(settings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
@@ -249,7 +264,7 @@ function createInitialState({ withStoredSession = true } = {}) {
startHint: '',
entrySettings: {
language: String(storedEntrySettings?.language || 'ru'),
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(storedEntrySettings?.solanaServer),
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
shineServerLogin: String(storedEntrySettings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(storedEntrySettings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
@@ -718,7 +733,7 @@ export function checkServerAvailability(address) {
}
export async function saveEntrySettings(nextSettings) {
const nextSolanaServer = String(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextSolanaServer = normalizeStoredSolanaServer(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextShineServerLogin = String(nextSettings?.shineServerLogin || state.entrySettings.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE).trim().toLowerCase()
|| DEFAULT_SHINE_SERVER_LOGIN_VALUE;
let forcedShineServer = LOCAL_WS_OVERRIDE_URL || '';