diff --git a/Dev_Docs/Pending_Features/2026-06-20_1839_registration_faq_and_12_words.md b/Dev_Docs/Pending_Features/2026-06-20_1839_registration_faq_and_12_words.md
new file mode 100644
index 0000000..64ade24
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-06-20_1839_registration_faq_and_12_words.md
@@ -0,0 +1,25 @@
+# Регистрация: FAQ и режим пароля из 12 слов
+
+- краткое описание:
+ - на экране регистрации добавлен блок частых вопросов с переходом на отдельный экран справки;
+ - добавлен альтернативный режим ввода пароля через 12 полей-слов в кошелёчном формате, которые склеиваются в одну строку без изменения API;
+ - такой же режим добавлен и на экран входа по логину и паролю.
+
+- что проверять:
+ - на стартовом экране открыть `Зарегистрироваться`;
+ - убедиться, что внизу экрана есть кнопки FAQ;
+ - открыть несколько вопросов и проверить возврат обратно на регистрацию;
+ - включить галочку `Представить пароль в виде 12 слов`;
+ - убедиться, что появляется сетка с нумерованными полями в 3 колонки;
+ - ввести часть слов, перейти дальше и проверить, что шаг подтверждения и генерация ключей работают;
+ - выключить галочку и проверить, что пароль остаётся собранным в одном поле;
+ - открыть экран входа по паролю и повторить те же проверки для режима `12 слов`;
+ - пройти регистрацию до шага оплаты без ошибок интерфейса.
+
+- ожидаемый результат:
+ - FAQ открывается отдельным экраном и содержит понятные ответы;
+ - режим `12 слов` не ломает регистрацию и вход и даёт тот же поток, что и обычный пароль;
+ - пароль не отправляется в новом формате, а продолжает использоваться как одна строка.
+
+- статус:
+ - pending
diff --git a/VERSION.properties b/VERSION.properties
index 9eb540d..0a1ce67 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.223
+client.version=1.2.225
server.version=1.2.211
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js
index 8e851ee..3d0348c 100644
--- a/shine-UI/js/app.js
+++ b/shine-UI/js/app.js
@@ -35,6 +35,7 @@ import {
import * as startView from './pages/start-view.js?v=202606142105';
import * as entrySettingsView from './pages/entry-settings-view.js?v=202606161240';
import * as registerView from './pages/register-view.js';
+import * as registrationFaqView from './pages/registration-faq-view.js';
import * as registrationPaymentView from './pages/registration-payment-view.js?v=202606180940';
import * as registrationKeysView from './pages/registration-keys-view.js';
import * as registrationDraftKeysView from './pages/registration-draft-keys-view.js';
@@ -81,6 +82,7 @@ const routes = {
'start-view': startView,
'entry-settings-view': entrySettingsView,
'register-view': registerView,
+ 'registration-faq-view': registrationFaqView,
'registration-payment-view': registrationPaymentView,
'registration-keys-view': registrationKeysView,
'registration-draft-keys-view': registrationDraftKeysView,
diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js
index 2c1541f..eef0a47 100644
--- a/shine-UI/js/pages/login-password-view.js
+++ b/shine-UI/js/pages/login-password-view.js
@@ -7,6 +7,55 @@ import {
state,
} from '../state.js';
import { toUserMessage } from '../services/ui-error-texts.js';
+import {
+ composePasswordFromWords,
+ emptyPasswordWords,
+ normalizePasswordWords,
+ PASSWORD_MAX_LENGTH,
+ PASSWORD_WORDS_COUNT,
+} from '../services/password-words.js';
+
+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, preview);
+ return { section, inputs, preview };
+}
export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false };
@@ -19,6 +68,9 @@ export function render({ navigate }) {
const form = document.createElement('div');
form.className = 'card stack';
+ let passwordMode = String(state.loginDraft.passwordMode || 'single') === 'words' ? 'words' : 'single';
+ let passwordWords = normalizePasswordWords(state.loginDraft.passwordWords);
+
const loginInput = document.createElement('input');
loginInput.className = 'input';
loginInput.type = 'text';
@@ -35,21 +87,47 @@ export function render({ navigate }) {
passwordInput.autocomplete = 'new-password';
passwordInput.autocapitalize = 'off';
passwordInput.spellcheck = false;
- passwordInput.value = state.loginDraft.password;
- passwordInput.placeholder = 'Введите пароль (можно оставить пустым)';
+ passwordInput.maxLength = PASSWORD_MAX_LENGTH;
+ passwordInput.value = passwordMode === 'single' ? state.loginDraft.password : '';
+ passwordInput.placeholder = 'Введите пароль';
+
+ const {
+ section: wordsSection,
+ inputs: wordInputs,
+ preview: wordsPreview,
+ } = createWordsLayout({
+ words: passwordWords,
+ onInput: (index, value) => {
+ passwordWords[index] = value;
+ syncDraftState();
+ },
+ });
+
+ 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 hint = document.createElement('p');
hint.className = 'meta-muted';
- hint.textContent = 'Введите логин. Пароль может быть пустым. На следующем шаге сохраните ключи на устройстве.';
+ hint.textContent = 'Введите логин. На следующем шаге сохраните ключи на устройстве.';
const advanced = document.createElement('details');
advanced.className = 'card stack';
advanced.innerHTML = `
Расширенные
-
Схема derivation ключей: логин нормализуется как trim().toLowerCase(). При непустом пароле используется Argon2id, затем из результата строится секрет для root/bch/dev ключей.
+
Схема деривации: логин нормализуется как trim().toLowerCase(). При непустом пароле используется Argon2id, затем из результата строится секрет.
+
Из секрета строятся root key, blockchain key и device key. Обычно можно не вникать в это подробно и просто хранить всё на своём устройстве.
+
Режим 12 слов ничего не меняет в протоколе: слова просто склеиваются в один обычный пароль длиной до 256 символов.
Если пароль пустой — используется прежний детерминированный режим совместимости.
-
Для тестов можно оставить пустой пароль.
-
Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MiB), p=1, dkLen=32. Выбор уровня будет добавлен позже.
Сейчас вашим первым и основным сервером будет серверный аккаунт ${state.entrySettings.shineServerLogin || 'shineupme'}.
Текущий адрес этого сервера: ${state.entrySettings.shineServerHttp || 'https://shineup.me'}.
-
При регистрации этот сервер будет записан в вашу PDA как первый сервер доступа.
-
При желании его можно изменить в настройках до регистрации или позже. В будущем будет поддерживаться мультисерверная модель, но сейчас используется один основной сервер.
+
Это первый сервер, на который вам будут писать и звонить после регистрации. Позже его можно будет сменить, а в будущем использовать и несколько серверов сразу.
Схема derivation ключей: при непустом пароле используется Argon2id (средний профиль), затем из результата строится секрет для root/bch/dev ключей.
-
В derivation участвуют и логин, и пароль: одинаковый пароль у разных логинов даёт разные ключи.
-
Если пароль пустой — используется прежний тестовый режим совместимости (старый детерминированный вариант).
-
Для тесто оставьте пустой пароль.
-
Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MB), p=1, dkLen=32. Выбор уровня будет добавлен позже.
+
Схема деривации: логин и пароль проходят через Argon2id, после чего получается главный секрет.
+
Из этого секрета строятся три ключа: root key для основной публичной записи и важных изменений, blockchain key для подписания действий SHiNE в блокчейне, device key для входа и работы конкретного устройства.
+
Разделение нужно, чтобы можно было аккуратнее выдавать права устройствам. Но если у вас нет большой суммы на счёте и нет повышенного риска, обычно можно просто хранить всё на своём устройстве.
+
Режим 12 слов не меняет формат пароля и не меняет API: слова просто склеиваются в одну строку длиной до 256 символов.
`;
const checkButton = document.createElement('button');
@@ -71,11 +171,49 @@ export function render({ navigate }) {
checkButton.type = 'button';
checkButton.textContent = 'Проверить логин';
+ const actions = document.createElement('div');
+ actions.className = 'auth-footer-actions';
+
+ const backButton = document.createElement('button');
+ backButton.className = 'ghost-btn';
+ backButton.type = 'button';
+ backButton.textContent = 'Назад';
+ backButton.addEventListener('click', () => navigate('start-view'));
+
+ const nextButton = document.createElement('button');
+ nextButton.className = 'primary-btn';
+ nextButton.type = 'button';
+ nextButton.textContent = 'Далее';
+
let lastCheckedLogin = '';
let lastCheckedFree = false;
let lastCheckedClassName = '';
let generationRunId = 0;
+ function getCurrentPassword() {
+ return passwordMode === 'words' ? composePasswordFromWords(passwordWords) : String(passwordInput.value || '');
+ }
+
+ function updateWordsPreview() {
+ const password = composePasswordFromWords(passwordWords);
+ const nonEmptyCount = normalizePasswordWords(passwordWords).filter((word) => word.trim()).length;
+ wordsPreview.textContent = `Заполнено слов: ${nonEmptyCount} из 12 · итоговая длина пароля: ${password.length} символов.`;
+ }
+
+ function updatePasswordModeVisibility() {
+ const wordsMode = passwordMode === 'words';
+ wordsSection.hidden = !wordsMode;
+ passwordInput.parentElement.hidden = wordsMode;
+ updateWordsPreview();
+ }
+
+ function syncDraftState() {
+ state.registrationDraft.login = String(loginInput.value.trim());
+ state.registrationDraft.passwordMode = passwordMode;
+ state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords);
+ state.registrationDraft.password = getCurrentPassword();
+ }
+
async function runAvailabilityCheck() {
const login = loginInput.value.trim();
if (!login) {
@@ -87,19 +225,19 @@ export function render({ navigate }) {
if (login === lastCheckedLogin) {
if (!lastCheckedFree) {
statusText.textContent = 'Логин уже занят ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else if (lastCheckedClassName === 'free') {
statusText.textContent = 'Логин свободен ✅';
- statusText.className = 'is-available';
+ statusText.className = 'status-line is-available';
} else if (lastCheckedClassName === 'premium') {
statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else if (lastCheckedClassName === 'company') {
statusText.textContent = 'Логин свободен, но относится к компании/бренду (отдельное согласование) ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else {
statusText.textContent = 'Логин нельзя использовать для обычной регистрации ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return lastCheckedFree && lastCheckedClassName === 'free';
@@ -132,21 +270,21 @@ export function render({ navigate }) {
lastCheckedClassName = className;
if (!isFree) {
statusText.textContent = 'Логин уже занят ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else if (className === 'free') {
statusText.textContent = precheckWarning
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
: 'Логин свободен ✅';
- statusText.className = 'is-available';
+ statusText.className = 'status-line is-available';
} else if (className === 'premium') {
statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else if (className === 'company') {
statusText.textContent = 'Логин свободен, но относится к компании/бренду (отдельное согласование) ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
} else {
statusText.textContent = 'Логин нельзя использовать для обычной регистрации ❌';
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return isFree && className === 'free';
@@ -154,7 +292,7 @@ export function render({ navigate }) {
const base = toUserMessage(error, 'Не удалось проверить логин');
const details = formatSolanaErrorDetails(error);
statusText.textContent = `${base}. Детали: ${details}`;
- statusText.className = 'is-unavailable';
+ statusText.className = 'status-line is-unavailable';
return false;
} finally {
checkButton.disabled = false;
@@ -164,19 +302,32 @@ export function render({ navigate }) {
checkButton.addEventListener('click', runAvailabilityCheck);
- const actions = document.createElement('div');
- actions.className = 'auth-footer-actions';
+ loginInput.addEventListener('input', () => {
+ syncDraftState();
+ lastCheckedLogin = '';
+ });
- const backButton = document.createElement('button');
- backButton.className = 'ghost-btn';
- backButton.type = 'button';
- backButton.textContent = 'Назад';
- backButton.addEventListener('click', () => navigate('start-view'));
+ passwordInput.addEventListener('input', () => {
+ syncDraftState();
+ });
+
+ passwordModeCheckbox.addEventListener('change', () => {
+ const nextMode = passwordModeCheckbox.checked ? 'words' : 'single';
+ if (nextMode === passwordMode) return;
+ if (nextMode === 'words') {
+ passwordWords = emptyPasswordWords();
+ wordInputs.forEach((input) => {
+ input.value = '';
+ });
+ passwordInput.value = '';
+ } else {
+ passwordInput.value = composePasswordFromWords(passwordWords);
+ }
+ passwordMode = nextMode;
+ updatePasswordModeVisibility();
+ syncDraftState();
+ });
- const nextButton = document.createElement('button');
- nextButton.className = 'primary-btn';
- nextButton.type = 'button';
- nextButton.textContent = 'Далее';
nextButton.addEventListener('click', async () => {
formError.style.display = 'none';
const isFree = await runAvailabilityCheck();
@@ -185,16 +336,23 @@ export function render({ navigate }) {
const prevLogin = String(state.registrationDraft.login || '');
const prevPassword = String(state.registrationDraft.password || '');
const nextLogin = String(loginInput.value.trim());
- const nextPassword = String(passwordInput.value || '');
+ const nextPassword = getCurrentPassword();
if (nextPassword.length === 0) {
formError.textContent = 'Пустой пароль запрещён. Введите непустой пароль для регистрации.';
formError.style.display = '';
return;
}
+ if (nextPassword.length > PASSWORD_MAX_LENGTH) {
+ formError.textContent = `Пароль получился слишком длинным. Максимальная длина: ${PASSWORD_MAX_LENGTH} символов.`;
+ formError.style.display = '';
+ return;
+ }
const credsChanged = prevLogin !== nextLogin || prevPassword !== nextPassword;
state.registrationDraft.login = nextLogin;
state.registrationDraft.password = nextPassword;
+ state.registrationDraft.passwordMode = passwordMode;
+ state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords);
if (credsChanged) {
state.registrationDraft.preGeneratedKeyBundle = null;
}
@@ -202,20 +360,18 @@ export function render({ navigate }) {
renderSecurityConfirmStage();
});
- actions.append(backButton, nextButton);
-
function renderInputStage() {
form.innerHTML = `
-
+
`;
form.children[0].append(loginInput);
form.children[1].append(passwordInput);
- form.append(serverNotice, checkButton, statusText, advanced, formError);
+ form.append(passwordModeToggle, wordsSection, serverNotice, checkButton, statusText, faqCard, advanced, formError);
actions.innerHTML = '';
actions.append(backButton, nextButton);
- backButton.disabled = false;
- nextButton.disabled = false;
+ updatePasswordModeVisibility();
+ syncDraftState();
}
function renderSecurityConfirmStage() {
@@ -223,8 +379,7 @@ export function render({ navigate }) {
const info = document.createElement('p');
info.className = 'auth-copy';
- info.textContent =
- 'Для повышения безопасности мы генерируем секрет из вашего пароля с помощью Argon2id.';
+ info.textContent = 'Для повышения безопасности мы генерируем секрет из вашего логина и пароля с помощью Argon2id.';
const details = document.createElement('p');
details.className = 'meta-muted';
@@ -232,14 +387,17 @@ export function render({ navigate }) {
const details2 = document.createElement('p');
details2.className = 'meta-muted';
- details2.textContent =
- 'Из этого секрета строятся root key, blockchain key и device key. Это может занять некоторое время.';
+ details2.textContent = 'Из этого секрета строятся root key, blockchain key и device key. Это может занять некоторое время.';
const details3 = document.createElement('p');
details3.className = 'meta-muted';
- details3.textContent = 'Это необходимо, чтобы усложнить подбор пароля и секрета.';
+ details3.textContent = 'Замедление нужно специально: оно усложняет подбор пароля и повышает цену атак на видеокартах и GPU.';
- form.append(info, details, details2, details3);
+ 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';
@@ -270,17 +428,10 @@ export function render({ navigate }) {
subtitle.textContent = 'Генерируется секрет из вашего пароля, из которого будут вычислены все ключи.';
const progressWrap = document.createElement('div');
- progressWrap.style.width = '100%';
- progressWrap.style.height = '10px';
- progressWrap.style.border = '1px solid rgba(180,180,180,.5)';
- progressWrap.style.borderRadius = '6px';
- progressWrap.style.overflow = 'hidden';
+ progressWrap.className = 'registration-progress';
const progressBar = document.createElement('div');
- progressBar.style.height = '100%';
- progressBar.style.width = '0%';
- progressBar.style.background = 'rgba(80, 160, 255, 0.9)';
- progressBar.style.transition = 'width 180ms linear';
+ progressBar.className = 'registration-progress-bar';
progressWrap.append(progressBar);
const progressText = document.createElement('p');
diff --git a/shine-UI/js/pages/registration-faq-view.js b/shine-UI/js/pages/registration-faq-view.js
new file mode 100644
index 0000000..e888962
--- /dev/null
+++ b/shine-UI/js/pages/registration-faq-view.js
@@ -0,0 +1,229 @@
+import { renderHeader } from '../components/header.js';
+import { state } from '../state.js';
+
+export const pageMeta = { id: 'registration-faq-view', title: 'Вопросы о регистрации', showAppChrome: false };
+
+export const REGISTRATION_FAQ_TOPICS = [
+ {
+ id: 'keys-storage',
+ shortTitle: 'Где ключи',
+ title: 'У кого хранятся ключи?',
+ paragraphs: [
+ 'Ключи хранятся только у вас: на вашем устройстве, на доверенных устройствах или на отдельном внешнем устройстве, которое вы контролируете сами.',
+ 'SHiNE не хранит ваши приватные ключи на сервере. Сервер помогает с доставкой и синхронизацией, но не владеет вашим секретом.',
+ 'Если захотите, ключи можно держать на отдельном полностью программируемом устройстве с открытым кодом, например на ESP32-контроллере.',
+ ],
+ },
+ {
+ id: 'reliability',
+ shortTitle: 'Надёжность',
+ title: 'Насколько это надёжно?',
+ paragraphs: [
+ 'Мы делаем ставку на открытость: клиентский код открыт, серверный код открыт, протокол открыт. Это позволяет проверять систему независимо, а не верить обещаниям на слово.',
+ 'Мы рекомендуем использовать браузеры с открытым исходным кодом. Позже планируются отдельные приложения для Android, iPhone, Ubuntu Touch и Linux, тоже с открытым кодом.',
+ 'Проект распространяется по лицензии AGPL v3. Часть важных данных и регистрационных записей также опирается на блокчейн-слой, чтобы уменьшать зависимость от одной закрытой стороны.',
+ ],
+ },
+ {
+ id: 'key-derivation',
+ shortTitle: 'Деривация',
+ title: 'Как генерируются ключи и что делает пароль?',
+ paragraphs: [
+ 'Из вашего логина и пароля с помощью Argon2id вычисляется специальный секрет.',
+ 'Уже из этого секрета детерминированно строятся три основных ключа: root key, blockchain key и device key.',
+ 'Это значит, что логин и пароль не просто проверяются на сервере, а реально участвуют в создании ваших ключей. У разных логинов даже с одинаковым паролем будут разные ключи.',
+ ],
+ },
+ {
+ id: 'three-keys',
+ shortTitle: 'Три ключа',
+ title: 'Зачем нужны три ключа?',
+ paragraphs: [
+ 'Root key нужен для управления вашей основной публичной записью и важными изменениями личности, включая обновление главной публичной части в Solana.',
+ 'Blockchain key нужен для подписания действий и записей в блокчейне SHiNE.',
+ 'Device key нужен для входов и работы конкретного устройства. Благодаря разделению ключей можно точнее выдавать права одним устройствам и не выдавать другим.',
+ 'Если не хочется в это вникать, обычно можно просто сохранить все ключи на своём устройстве. Для большинства обычных сценариев на iPhone, Android и Linux это вполне практично. Для больших сумм или повышенного риска лучше отдельное внешнее устройство.',
+ ],
+ },
+ {
+ id: 'generation-time',
+ shortTitle: 'Зачем время',
+ title: 'Зачем нужна заметная пауза при генерации?',
+ paragraphs: [
+ 'Генерация специально сделана не мгновенной. Это усложняет массовый подбор паролей.',
+ 'Argon2id расходует и время, и память, поэтому атаки на GPU и видеокартах становятся заметно дороже и медленнее.',
+ 'Небольшая задержка при создании секрета здесь работает как дополнительная защита.',
+ ],
+ },
+ {
+ id: 'strong-password',
+ shortTitle: 'Какой пароль',
+ title: 'Какой пароль считается надёжным?',
+ paragraphs: [
+ 'Минимально разумный уровень сейчас начинается примерно от 8 символов.',
+ 'Хороший практический ориентир для большинства людей: 12 символов и больше. Пароль у нас может быть длиной до 256 символов.',
+ 'Если вам удобнее думать словами, можно использовать режим из 12 полей ниже: слова просто склеиваются в один длинный пароль, и система не проверяет орфографию.',
+ ],
+ },
+ {
+ id: 'one-or-twelve',
+ shortTitle: '1 или 12 слов',
+ title: 'Чем отличается один пароль от режима 12 слов?',
+ paragraphs: [
+ 'Технически ничем: это один и тот же пароль. Режим 12 слов нужен только для удобства запоминания и ввода.',
+ 'Можно заполнить все 12 полей, можно только первые 6, можно использовать слова от другого кошелька, разные языки и любые нестандартные символы.',
+ 'Главное помнить, что в конце всё равно получается одна строка длиной до 256 символов.',
+ ],
+ },
+ {
+ id: 'why-own-password',
+ shortTitle: 'Зачем свой',
+ title: 'Почему лучше иметь свой пароль и свои ключи?',
+ paragraphs: [
+ 'Чем дальше, тем проще будет подделывать фотографию, голос, интонацию и даже другие привычные признаки личности с помощью нейросетей.',
+ 'На расстоянии всё сложнее будет понять, что перед вами действительно вы, если опираться только на внешние признаки.',
+ 'Поэтому персональные ключи, которые храните только вы, становятся надёжнее, чем зависимость от сторонней организации, которая держит ключи у себя.',
+ ],
+ },
+ {
+ id: 'first-server',
+ shortTitle: 'Первый сервер',
+ title: 'Что такое первый сервер SHiNE?',
+ paragraphs: [
+ 'Первый сервер SHiNE это тот сервер, на который вам будут писать и звонить в самом начале. При регистрации он записывается как ваш первый сервер доступа.',
+ 'Позже вы сможете сменить сервер, а ваши данные останутся с вами. В будущем серверов может быть несколько одновременно.',
+ 'Если серверов несколько, данные между ними будут синхронизироваться автоматически. Если добавляете новый сервер и убираете старый, просто дождитесь завершения синхронизации перед отключением старого.',
+ 'Если у вас не остаётся ни одного сервера, синхронизации, конечно, не будет, пока не появится хотя бы один активный сервер снова.',
+ ],
+ },
+ {
+ id: 'hardware-device',
+ shortTitle: 'ESP32',
+ title: 'Нужно ли отдельное устройство для ключей?',
+ paragraphs: [
+ 'Идеальный вариант для важных ключей: отдельное физическое устройство, которое вы контролируете сами.',
+ 'Если пока не хотите покупать отдельное устройство, можно пользоваться телефоном. Но отдельный контроллер или мини-устройство обычно даёт лучший контроль и более понятную модель доверия.',
+ 'Красивая готовая модель Waveshare на ESP32-S3 Touch AMOLED 2.16 стоит около 32 долларов. Есть и более дешёвые варианты на открытых чипах, примерно от 10 до 15 долларов.',
+ 'Если у вас другая модель, под неё можно адаптировать открытую прошивку. Для простых переносов это реально сделать довольно быстро.',
+ ],
+ links: [
+ {
+ label: 'Документация Waveshare ESP32-S3 Touch AMOLED 2.16',
+ href: 'https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16',
+ },
+ ],
+ },
+ {
+ id: 'wallet-device',
+ shortTitle: 'Кошелёк',
+ title: 'Можно ли использовать такое устройство как кошелёк?',
+ paragraphs: [
+ 'Да. Идея SHiNE в том, что устройство может подписывать не только внутренние действия, но и любые другие данные, если для этого добавлена нужная логика.',
+ 'То есть это направление совместимо с моделью аппаратного кошелька: вы храните ключи у себя, а устройство подписывает то, что вы разрешили.',
+ 'Пока ещё не все валюты и сценарии доведены до готового пользовательского уровня, но архитектурно это именно путь к универсальному подписывающему устройству.',
+ ],
+ },
+];
+
+function getTopicById(topicId) {
+ return REGISTRATION_FAQ_TOPICS.find((topic) => topic.id === topicId) || REGISTRATION_FAQ_TOPICS[0];
+}
+
+export function openRegistrationFaq(navigate, topicId) {
+ state.registrationHelp.selectedTopic = getTopicById(topicId).id;
+ navigate('registration-faq-view');
+}
+
+export function render({ navigate }) {
+ const selectedTopic = getTopicById(state.registrationHelp?.selectedTopic);
+ const screen = document.createElement('section');
+ screen.className = 'stack';
+
+ const heroCard = document.createElement('div');
+ heroCard.className = 'card stack registration-faq-hero';
+ heroCard.innerHTML = `
+
Вопросы о регистрации
+
Короткие ответы на самые частые вопросы о ключах, пароле, первом сервере и доверенных устройствах.