diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 81 | ||||
| -rw-r--r-- | src/i18n.js | 4 |
2 files changed, 64 insertions, 21 deletions
@@ -1338,14 +1338,31 @@ function unitTag(cls = '') { // ================================================================ UNLOCK // The create/import card on its own — the unlock screen wraps it in the // classic chrome, the onboarding wizard's "Get started" step shows it bare. +// The front door's seed pages (first run, or "Sign into another account"): +// no tabs, and the page's own Back steps through the create flow. +function seedPages() { + return ui.screen === 'unlock' && !!((ui.onb && ui.onb.step === 'seed') || (ui.fromWallet && ui.addFlow)); +} +// Back within the create flow: the check → the passphrase → the words → the +// heads-up. False when there's no earlier step (the caller leaves the flow). +const CREATE_PREV = { gen: 'intro', pass: 'gen', confirm: 'pass' }; +function createStepBack() { + if (ui.unlockTab !== 'create' || ui.entropyPage || !CREATE_PREV[ui.createStep]) return false; + ui.createStep = CREATE_PREV[ui.createStep]; + ui.unlockError = ''; + render(); + return true; +} + function unlockCard() { // The entropy page takes the card over entirely — seed words, tabs and // other controls step aside until Submit or Back. if (ui.entropyPage) return h('div', { class: 'card col' }, entropyPage()); // Inside the first-run wizard the words are already on screen: no tabs, // just a line underneath for whoever brought their own seed (and one back). - const wizard = ui.screen === 'unlock' && ((ui.onb && ui.onb.step === 'seed') || (ui.fromWallet && ui.addFlow)); - const swap = wizard && ui.createStep !== 'confirm' ? h('button', { + const wizard = seedPages(); + // only before the words are up: once they're shown, it was already offered + const swap = wizard && (ui.unlockTab === 'import' || ui.createStep === 'intro') ? h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { clearSeedDrafts(); ui.unlockError = ''; @@ -1434,6 +1451,7 @@ function unlockScreen() { h('h2', { class: 'onb-title' }, t(ui.unlockTab === 'import' ? 'onbRestoreTitle' : 'onbSeedTitle')), unlockCard(), ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { + if (createStepBack()) return; const from = ui.unlockTab === 'import' ? 'doors' : 'welcome'; clearSeedDrafts(); ui.draftMnemonic = ''; ui.createStep = 'intro'; ui.confirm = []; ui.unlockError = ''; ui.addStep = from; render(); @@ -1603,33 +1621,47 @@ function createPane() { h('button', { class: 'btn-block', style: 'display:flex;align-items:center;justify-content:center;gap:8px', onClick: () => copy(ui.draftMnemonic) }, h('span', { style: 'display:flex', html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>' }), t('copy')), - optionsPanel(), h( 'button', { id: 'seed-next', class: 'btn-primary btn-block', - onClick: () => { - // catch a mistyped passphrase here, while the field to fix it is - // still on screen — the check only asks for one word - if (ui.passphrase && ui.confirmPass !== ui.passphrase) { - ui.unlockError = t('passphraseMismatch'); render(); return; - } - ui.confirm = pickConfirm(words); - ui.unlockError = ''; - ui.createStep = 'confirm'; - render(); - }, + onClick: () => { ui.unlockError = ''; ui.createStep = 'pass'; render(); }, }, t('next') ) ); } + // The passphrase, on a page of its own (Adam, 2026-11-07): what it is, + // that it's optional but recommended, and the field — a small "Add + // passphrase" button under the words was easy to miss. + if (ui.createStep === 'pass') { + const words = ui.draftMnemonic.split(' '); + const next = () => { + // a typo here IS a different wallet: caught while the fields are up + if (ui.passphrase && ui.confirmPass !== ui.passphrase) { + ui.unlockError = t('passphraseMismatch'); render(); return; + } + ui.confirm = pickConfirm(words); + ui.unlockError = ''; + ui.createStep = 'confirm'; + render(); + }; + return h( + 'div', + { class: 'col' }, + h('h3', { style: 'margin:0' }, t('passStepTitle')), + h('p', { style: 'margin:0' }, t('passStepBody', { n: words.length })), + h('p', { class: 'muted', style: 'margin:0' }, t('passStepNote')), + ...passFields(), + h('button', { id: 'pass-next', class: 'btn-primary btn-block', onClick: next }, t(ui.passphrase ? 'next' : 'passStepNone')), + ); + } // The check: which word is #5? One tap from a few of the phrase's own // words, or Skip (Adam, 2026-11-07) — no typing. const q = ui.confirm; - if (!q || !q.options) { ui.createStep = 'gen'; return createPane(); } + if (!q || !q.options) { ui.createStep = 'pass'; return createPane(); } return h( 'div', { class: 'col' }, @@ -1647,7 +1679,8 @@ function createPane() { }, w))), q.wrong && q.wrong.length ? h('div', { class: 'notice err' }, t('quizWrong')) : null, h('button', { id: 'seed-skip', class: 'btn-ghost btn-block', onClick: () => { ui.unlockError = ''; openWallet(ui.draftMnemonic, { generated: true }); } }, t('skip')), - h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { ui.createStep = 'gen'; ui.unlockError = ''; render(); } }, t('quizBack')), + // outside the seed pages (no Back of their own), a way back to the words + seedPages() ? null : h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { ui.createStep = 'pass'; ui.unlockError = ''; render(); } }, t('back')), ); } @@ -1753,22 +1786,28 @@ function optionsPanel() { // already typed keeps it open. if (!ui.passOpen && !ui.passphrase) { return [h('button', { - class: 'btn-ghost btn-sm', type: 'button', style: 'align-self:flex-start', + class: 'btn-block', type: 'button', onClick: (e) => { e.preventDefault(); ui.passOpen = true; render(); }, }, t('addPassphrase'))]; } + return passFields({ label: true }); +} + +// The passphrase field (+ its confirmation once something is typed). The +// create flow's own page explains it above; the import pane labels it here. +function passFields({ label = false } = {}) { const pass = h( 'label', { class: 'field' }, h('span', { class: 'lab' }, - t('passphraseOptional'), + t(label ? 'passphraseOptional' : 'passphrase'), // People assume a password logs in or encrypts; a BIP39 passphrase does // neither, and getting that wrong loses money — worth a sentence here. h('button', { class: 'linklike small', type: 'button', style: 'margin-left:8px', onClick: (e) => { e.preventDefault(); ui.passInfoOpen = !ui.passInfoOpen; render(); }, }, t('passWhatsThis'))), - ui.passInfoOpen ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passInfo')) : h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passOptionalHint')), + ui.passInfoOpen ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passInfo')) : label ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passOptionalHint')) : null, h( 'div', { class: 'input-group' }, @@ -1797,7 +1836,7 @@ function optionsPanel() { class: 'mono-input', autocomplete: 'off', value: ui.confirmPass, - onInput: (e) => (ui.confirmPass = e.target.value), + onInput: (e) => { ui.confirmPass = e.target.value; if (ui.unlockError && ui.confirmPass === ui.passphrase) { ui.unlockError = ''; render(); } }, }) ) : null; @@ -4412,7 +4451,7 @@ function onboardScreen() { title(t(ui.unlockTab === 'import' ? 'onbRestoreTitle' : 'onbSeedTitle')), unlockCard(), // the entropy page brings its own Back — one is enough - ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { ui.unlockError = ''; o.step = ui.unlockTab === 'import' ? 'doors' : 'welcome'; render(); } }, t('back')), + ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { if (createStepBack()) return; ui.unlockError = ''; o.step = ui.unlockTab === 'import' ? 'doors' : 'welcome'; render(); } }, t('back')), ]); } if (o.step === 'signin') { diff --git a/src/i18n.js b/src/i18n.js index a985ff9..b45638e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -201,6 +201,10 @@ const STR = { seedIntroPrivate: 'Anyone who sees them can take your coins, so make sure no one is looking at your screen.', seedIntroShow: 'Show seed phrase', next: 'Next', + passStepTitle: 'Add a passphrase?', + passStepBody: 'A passphrase is an extra secret you choose yourself, on top of your {n} words. Anyone who finds your seed phrase still can’t open your wallet without it.', + passStepNote: 'It’s optional, but recommended. Write it down with your seed phrase: you’ll need both to recover your wallet, exactly as written, and nobody can reset it for you.', + passStepNone: 'Continue without one', skip: 'Skip', quizPrompt: 'Which one is word #{n}?', quizWrong: 'Not that one. Check what you wrote down.', |
