aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-07 08:47:18 -0700
committerAdam Soltys <[email protected]>2026-11-07 08:47:18 -0700
commitb3a5ea14585a25e6fc7c9e35f94fcd3e1025a543 (patch)
tree7a614895052820f00621b7078e109622f5b5987d /src/app.js
parentd8f90811503d5dd9e827727f52bc10fa3317803f (diff)
downloadcoinosv3-b3a5ea14585a25e6fc7c9e35f94fcd3e1025a543.tar.gz
coinosv3-b3a5ea14585a25e6fc7c9e35f94fcd3e1025a543.zip
The passphrase gets a page of its own in the create flow, between the words and the check. "The add passphrase button is too small … maybe it should get its own separate prompt screen with explainer of what it is and letting people know it's optional but recommended. we don't need a 'show previous screen' link on verify page because there's already a back button. 'Have an existing seed or entropy to import?' can go away once the seed is displayed" (Adam, 2026-11-07). New wallets go heads-up → words (Copy, Next) → "Add a passphrase?" (an extra secret on top of the 12 words; optional but recommended; write it down with the seed, nobody can reset it), with the field, its confirmation once typed, and one button that reads "Continue without one" until something is typed, then Next; a mismatch is caught there and clears once the fields agree → "Which one is word #5?". The page's own Back now steps through those (check → passphrase → words → heads-up, keeping what was typed) on the first-run and "Sign into another account" pages, so the check drops "Show the words again"; the classic card, which has no Back of its own, keeps one. "Have an existing seed or entropy to import?" shows only on the heads-up (and its "create instead" twin on the import page). The import pane's Add passphrase is a full-width button. passFields() split out of optionsPanel
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js81
1 files changed, 60 insertions, 21 deletions
diff --git a/src/app.js b/src/app.js
index 3ed0b33..23a1d42 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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') {