SHA256
Merge origin/main (+5: финал регистрации, автовход, порядок в deploy)
- register-view: принят вариант агента для видимости пароля — поле видно всегда, слова «12 слов» автоматически склеиваются в поле (проверено: оба режима, значение сохраняется при переключении). - Остальное авто-слито (deploy-схема server2, AGENTS, entry-settings, state). - VERSION: client 1.2.326, server 1.2.297. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,9 @@ export function render({ navigate }) {
|
||||
draft.shineServer = resolved.wsUrl;
|
||||
applyStatus(resolved.status, resolved.httpBase);
|
||||
} else {
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value, {
|
||||
solanaEndpoint: draft.solanaServer,
|
||||
});
|
||||
applyStatus(next);
|
||||
}
|
||||
} finally {
|
||||
@@ -188,16 +190,6 @@ export function render({ navigate }) {
|
||||
|
||||
actions.append(serverUiButton, cancelButton, saveButton);
|
||||
|
||||
const help = document.createElement('button');
|
||||
help.className = 'help-fab';
|
||||
help.type = 'button';
|
||||
help.textContent = '?';
|
||||
help.addEventListener('click', () => {
|
||||
window.alert(
|
||||
'Текст для разработчиков: для SHiNE вводится логин серверного аккаунта. Клиент читает его PDA, берёт server_address, показывает точный https-адрес и проверяет доступность WS-канала автоматически.',
|
||||
);
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки входа',
|
||||
@@ -205,7 +197,6 @@ export function render({ navigate }) {
|
||||
}),
|
||||
body,
|
||||
actions,
|
||||
help,
|
||||
);
|
||||
|
||||
screen.cleanup = () => {
|
||||
|
||||
@@ -163,6 +163,7 @@ export function render({ navigate }) {
|
||||
words: passwordWords,
|
||||
onInput: (index, value) => {
|
||||
passwordWords[index] = value;
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
passwordWordsLinked = true;
|
||||
syncDraftState();
|
||||
updateWordsPreview();
|
||||
@@ -247,11 +248,13 @@ export function render({ navigate }) {
|
||||
let generationRunId = 0;
|
||||
|
||||
function getCurrentPassword() {
|
||||
return passwordMode === 'words' ? composePasswordFromWords(passwordWords) : String(passwordInput.value || '');
|
||||
return String(passwordInput.value || '');
|
||||
}
|
||||
|
||||
function updateWordsPreview() {
|
||||
const password = getCurrentPassword();
|
||||
const password = passwordMode === 'words'
|
||||
? composePasswordFromWords(passwordWords)
|
||||
: String(passwordInput.value || '');
|
||||
const text = `Итоговая длина пароля: ${password.length} символов.`;
|
||||
wordsPreview.textContent = text;
|
||||
passwordLengthText.textContent = text;
|
||||
@@ -295,9 +298,8 @@ export function render({ navigate }) {
|
||||
function updatePasswordModeVisibility() {
|
||||
const wordsMode = passwordMode === 'words';
|
||||
wordsSection.style.display = wordsMode ? 'grid' : 'none';
|
||||
if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid';
|
||||
// Строка ввода видна в обычном режиме и скрыта в режиме «12 слов» (раньше условие было перевёрнуто — поле пропадало всегда).
|
||||
passwordInputRow.style.display = wordsMode ? 'none' : 'grid';
|
||||
if (passwordField) passwordField.style.display = 'grid';
|
||||
passwordInputRow.style.display = 'grid';
|
||||
updateWordsPreview();
|
||||
}
|
||||
|
||||
@@ -385,7 +387,6 @@ export function render({ navigate }) {
|
||||
if (runId !== loginCheckRunId) return false;
|
||||
const isFree = !check.exists;
|
||||
let className = '';
|
||||
let precheckWarning = '';
|
||||
if (isFree) {
|
||||
if (promoAccepted) {
|
||||
className = 'promo';
|
||||
@@ -398,7 +399,7 @@ export function render({ navigate }) {
|
||||
className = precheck.className;
|
||||
} catch (precheckError) {
|
||||
className = 'free';
|
||||
precheckWarning = formatSolanaErrorDetails(precheckError);
|
||||
console.warn('Solana login precheck fallback to free', formatSolanaErrorDetails(precheckError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,12 +411,7 @@ export function render({ navigate }) {
|
||||
} else if (className === 'promo') {
|
||||
setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available');
|
||||
} else if (className === 'free') {
|
||||
setStatusMessage(
|
||||
precheckWarning
|
||||
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
|
||||
: 'Логин свободен ✅',
|
||||
'is-available',
|
||||
);
|
||||
setStatusMessage('Логин свободен ✅', 'is-available');
|
||||
} else if (className === 'premium') {
|
||||
setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable');
|
||||
} else if (className === 'company') {
|
||||
@@ -462,11 +458,11 @@ export function render({ navigate }) {
|
||||
wordInputs.forEach((input) => {
|
||||
input.value = '';
|
||||
});
|
||||
passwordInput.value = '';
|
||||
} else {
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
}
|
||||
} else {
|
||||
} else if (passwordWordsLinked) {
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
passwordWordsLinked = true;
|
||||
}
|
||||
passwordMode = nextMode;
|
||||
updatePasswordModeVisibility();
|
||||
|
||||
@@ -24,7 +24,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 = 12;
|
||||
const AUTO_LOGIN_INITIAL_DELAY_MS = 5000;
|
||||
const AUTO_LOGIN_RETRY_MS = 1000;
|
||||
const EMPTY_PASSWORD_WORDS = Array.from({ length: 12 }, () => '');
|
||||
|
||||
function parseBalanceSol(value) {
|
||||
@@ -329,82 +330,76 @@ function renderSolanaDoneStage({ navigate, status, keyBundle }) {
|
||||
const card = screen.querySelector('.card.stack');
|
||||
if (!card) return;
|
||||
|
||||
let remainingSec = SOLANA_SYNC_WAIT_SEC;
|
||||
let timerId = null;
|
||||
let loginStarted = false;
|
||||
let initialDelayId = null;
|
||||
let retryTimerId = null;
|
||||
let loginInFlight = false;
|
||||
let loginCompleted = false;
|
||||
|
||||
const info = document.createElement('p');
|
||||
info.className = 'auth-copy';
|
||||
info.textContent = 'Регистрация завершена успешно.';
|
||||
const title = document.createElement('h2');
|
||||
title.textContent = 'Поздравляем, регистрация завершена';
|
||||
title.style.margin = '0';
|
||||
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'meta-muted';
|
||||
hint.textContent = 'Нужно подождать 10–15 секунд, пока в блокчейне SHiNE обновится ваша запись о регистрации. После этого мы автоматически войдём в ваш аккаунт.';
|
||||
hint.className = 'auth-copy';
|
||||
hint.textContent = 'Осталось подождать несколько секунд, пока обновится запись в блокчейне. После этого приложение автоматически войдёт в ваш аккаунт.';
|
||||
|
||||
const timer = document.createElement('p');
|
||||
timer.className = 'meta-muted';
|
||||
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
|
||||
const progress = document.createElement('p');
|
||||
progress.className = 'meta-muted';
|
||||
progress.textContent = 'Подготавливаем автоматический вход...';
|
||||
|
||||
const tryLoginBtn = document.createElement('button');
|
||||
tryLoginBtn.className = 'primary-btn';
|
||||
tryLoginBtn.type = 'button';
|
||||
tryLoginBtn.textContent = 'Входим автоматически...';
|
||||
tryLoginBtn.disabled = true;
|
||||
const altHint = document.createElement('p');
|
||||
altHint.className = 'meta-muted';
|
||||
altHint.textContent = 'Или вы можете';
|
||||
|
||||
const statusHint = document.createElement('p');
|
||||
statusHint.className = 'meta-muted';
|
||||
statusHint.textContent = 'Ничего нажимать не нужно. Если сервер ответит раньше, войдём сразу.';
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.className = 'ghost-btn';
|
||||
backBtn.type = 'button';
|
||||
backBtn.textContent = 'Вернуться в главное меню';
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timerId) {
|
||||
window.clearInterval(timerId);
|
||||
timerId = null;
|
||||
const stopAutoLogin = () => {
|
||||
if (initialDelayId) {
|
||||
window.clearTimeout(initialDelayId);
|
||||
initialDelayId = null;
|
||||
}
|
||||
if (retryTimerId) {
|
||||
window.clearInterval(retryTimerId);
|
||||
retryTimerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoLogin = async () => {
|
||||
if (loginStarted) return;
|
||||
loginStarted = true;
|
||||
stopTimer();
|
||||
const tryAutoLogin = async () => {
|
||||
if (loginCompleted || loginInFlight) return;
|
||||
loginInFlight = true;
|
||||
progress.textContent = 'Пробуем автоматически войти в аккаунт...';
|
||||
status.style.display = 'none';
|
||||
timer.textContent = 'Пробуем автоматически войти...';
|
||||
tryLoginBtn.textContent = 'Входим...';
|
||||
try {
|
||||
await completeRegistrationLogin({ navigate, keyBundle });
|
||||
loginCompleted = true;
|
||||
stopAutoLogin();
|
||||
} catch (error) {
|
||||
loginStarted = false;
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = toUserMessage(error, 'Пока не удалось автоматически войти. Попробуйте ещё раз через несколько секунд.');
|
||||
status.style.display = '';
|
||||
timer.textContent = 'Автовход пока не удался.';
|
||||
tryLoginBtn.disabled = false;
|
||||
tryLoginBtn.textContent = 'Попробовать войти сейчас';
|
||||
if (!loginCompleted) {
|
||||
progress.textContent = 'Ждём обновления записи и скоро попробуем снова...';
|
||||
console.warn('Auto login after registration is not ready yet', toUserMessage(error, 'auto-login-wait'));
|
||||
}
|
||||
} finally {
|
||||
loginInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateTimerUi = () => {
|
||||
if (remainingSec > 0) {
|
||||
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
|
||||
tryLoginBtn.textContent = `Автовход через ${remainingSec} сек`;
|
||||
tryLoginBtn.disabled = true;
|
||||
} else {
|
||||
timer.textContent = 'Запускаем автоматический вход...';
|
||||
tryLoginBtn.textContent = 'Входим...';
|
||||
tryLoginBtn.disabled = true;
|
||||
void startAutoLogin();
|
||||
}
|
||||
};
|
||||
|
||||
tryLoginBtn.addEventListener('click', async () => {
|
||||
if (loginStarted) return;
|
||||
await startAutoLogin();
|
||||
backBtn.addEventListener('click', () => {
|
||||
loginCompleted = true;
|
||||
stopAutoLogin();
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
card.innerHTML = '';
|
||||
card.append(info, hint, statusHint, timer, tryLoginBtn, status);
|
||||
updateTimerUi();
|
||||
timerId = window.setInterval(() => {
|
||||
remainingSec -= 1;
|
||||
updateTimerUi();
|
||||
}, 1000);
|
||||
card.append(title, hint, progress, altHint, backBtn, status);
|
||||
|
||||
initialDelayId = window.setTimeout(() => {
|
||||
if (loginCompleted) return;
|
||||
void tryAutoLogin();
|
||||
retryTimerId = window.setInterval(() => {
|
||||
void tryAutoLogin();
|
||||
}, AUTO_LOGIN_RETRY_MS);
|
||||
}, AUTO_LOGIN_INITIAL_DELAY_MS);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,9 @@ export function render({ navigate }) {
|
||||
draft.shineServer = resolved.wsUrl;
|
||||
applyStatus(resolved.status, resolved.httpBase);
|
||||
} else {
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value, {
|
||||
solanaEndpoint: draft.solanaServer,
|
||||
});
|
||||
applyStatus(next);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveShineServerByServerLogin } from './shine-server-resolver.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
function normalizeUrl(value) {
|
||||
return String(value || '').trim();
|
||||
@@ -121,12 +121,13 @@ export async function resolveAndCheckShineServerLogin(serverLogin, solanaEndpoin
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkServerAvailabilityByKey(key, url) {
|
||||
export async function checkServerAvailabilityByKey(key, url, options = {}) {
|
||||
const selectedSolanaEndpoint = normalizeUrl(options?.solanaEndpoint || state.entrySettings.solanaServer);
|
||||
if (key === 'solanaServer') {
|
||||
return (await checkSolanaRpc(url)) ? 'available' : 'unavailable';
|
||||
}
|
||||
if (key === 'shineServerLogin') {
|
||||
return (await resolveAndCheckShineServerLogin(url, SOLANA_ENDPOINT_DEFAULT)).status;
|
||||
return (await resolveAndCheckShineServerLogin(url, selectedSolanaEndpoint)).status;
|
||||
}
|
||||
if (key === 'shineServer') {
|
||||
return (await checkShineWs(url)) ? 'available' : 'unavailable';
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
||||
|
||||
const CLASSIFY_LOGIN_INSTRUCTION_TAG = 1;
|
||||
const PRECHECK_SIM_PAYER = 'SHiJ58FvbJQJnPEUa5mZNVpWd1ym9CkjjPgeHYzUJmh';
|
||||
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
||||
|
||||
let solanaLibPromise = null;
|
||||
@@ -86,7 +85,6 @@ export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
||||
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
|
||||
const payer = new solana.PublicKey(PRECHECK_SIM_PAYER);
|
||||
const ix = new solana.TransactionInstruction({
|
||||
programId: loginGuardProgram,
|
||||
keys: [],
|
||||
@@ -94,7 +92,9 @@ export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||
});
|
||||
const { blockhash } = await connection.getLatestBlockhash('confirmed');
|
||||
const v0Message = new solana.TransactionMessage({
|
||||
payerKey: payer,
|
||||
// Для simulateTransaction нужен существующий аккаунт payer.
|
||||
// Используем сам program id, чтобы не зависеть от внешнего тестового адреса.
|
||||
payerKey: loginGuardProgram,
|
||||
recentBlockhash: blockhash,
|
||||
instructions: [ix],
|
||||
}).compileToV0Message();
|
||||
|
||||
+1
-10
@@ -100,16 +100,7 @@ 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;
|
||||
return raw.replace(/\/+$/u, '');
|
||||
}
|
||||
|
||||
export function normalizeDmChatId(value) {
|
||||
|
||||
Reference in New Issue
Block a user