diff options
| -rw-r--r-- | src/app.js | 144 | ||||
| -rw-r--r-- | src/features/gifts.js | 64 | ||||
| -rw-r--r-- | src/i18n.js | 9 | ||||
| -rw-r--r-- | src/style.css | 4 | ||||
| -rw-r--r-- | tools/gift-profile-setup-test.js | 37 | ||||
| -rw-r--r-- | tools/onboard-backup-test.js | 10 |
6 files changed, 151 insertions, 117 deletions
@@ -77,7 +77,7 @@ const ui = { confirmClear: false, // "Clear all" confirmation shown editId: null, // account being renamed editLabel: '', - createStep: 'gen', // 'gen' | 'confirm' + createStep: 'intro', // 'intro' (the heads-up) | 'gen' (the words) | 'confirm' (the one-word check) draftMnemonic: '', confirm: [], // [{ index, value }] confirmPass: '', // re-entered passphrase (confirm field, shown once one is typed) @@ -1345,12 +1345,12 @@ function unlockCard() { // 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'; - const swap = wizard ? h('button', { + const swap = wizard && ui.createStep !== 'confirm' ? h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { clearSeedDrafts(); ui.unlockError = ''; if (ui.unlockTab === 'create') ui.unlockTab = 'import'; - else { ui.unlockTab = 'create'; ui.createStep = 'gen'; ui.draftRandom = ui.draftMnemonic = newMnemonic(); } + else { ui.unlockTab = 'create'; ui.createStep = 'intro'; ui.draftRandom = ui.draftMnemonic = newMnemonic(); } render(); }, }, ui.unlockTab === 'create' ? t('onbHaveSeed') : t('onbCreateInstead')) : null; @@ -1476,6 +1476,27 @@ function tabBtn(label, active, onClick) { } function createPane() { + // A heads-up before the words appear: they are secret, and the screen is + // about to show them (Adam, 2026-11-07). No warning icon — a plain note. + if (ui.createStep === 'intro' || !ui.createStep) { + const n = ui.draftMnemonic ? ui.draftMnemonic.split(' ').length : 12; + return h( + 'div', + { class: 'col seed-intro' }, + h('p', { style: 'margin:0' }, t('seedIntroBody', { n })), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { + id: 'seed-intro-next', + class: 'btn-primary btn-block', style: 'padding:14px', + onClick: () => { + if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic(); + ui.unlockError = ''; + ui.createStep = 'gen'; + render(); + }, + }, t('seedIntroShow')), + ); + } if (ui.createStep === 'gen') { // The wizard shows words, never a Generate button: a refresh restores // the step but not the draft (drafts live in memory only), so draw a @@ -1517,10 +1538,11 @@ function createPane() { 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 verify step only samples the words + // still on screen — the check only asks for one word if (ui.passphrase && ui.confirmPass !== ui.passphrase) { ui.unlockError = t('passphraseMismatch'); render(); return; } @@ -1530,57 +1552,45 @@ function createPane() { render(); }, }, - t('verifyBackup') - ), - h( - 'button', - { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic, { generated: true }) }, - t('skipVerification') + t('next') ) ); } - // confirm step (optional — reachable via "Verify backup") + // 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(); } return h( 'div', { class: 'col' }, - h('p', { class: 'muted' }, t('confirmBackupIntro')), - ...ui.confirm.map((c, i) => - h( - 'label', - { class: 'field' }, - h('span', { class: 'lab' }, t('wordN', { n: c.index + 1 })), - h('input', { - type: 'text', - class: 'mono-input', - autocapitalize: 'none', - autocomplete: 'off', - spellcheck: 'false', - value: c.value, - onInput: (e) => (ui.confirm[i].value = e.target.value.trim()), - }) - ) - ), - h('div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.createStep = 'gen'; render(); } }, t('back')), + h('p', { style: 'margin:0' }, t('quizPrompt', { n: q.index + 1 })), + h('div', { class: 'quiz-opts' }, q.options.map((w) => h('button', { - class: 'btn-primary grow', + type: 'button', + class: 'quiz-opt' + (q.wrong && q.wrong.includes(w) ? ' wrong' : ''), onClick: () => { const words = ui.draftMnemonic.split(' '); - const ok = ui.confirm.every((c) => c.value.toLowerCase() === words[c.index]); - if (!ok) { ui.unlockError = t('wordsMismatch'); render(); return; } - openWallet(ui.draftMnemonic, { generated: true }); + if (w === words[q.index]) { ui.unlockError = ''; openWallet(ui.draftMnemonic, { generated: true }); return; } + (q.wrong ||= []).includes(w) || q.wrong.push(w); + render(); }, - }, t('openWallet')) - ), - h('button', { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic, { generated: true }) }, t('skipVerification')) + }, 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')), ); } -function pickConfirm(words) { - const idx = new Set(); - while (idx.size < 3) idx.add(Math.floor(Math.random() * words.length)); - return [...idx].sort((a, b) => a - b).map((index) => ({ index, value: '' })); +// The 5th word and three others from the same phrase, shuffled: knowing the +// words isn't enough, you need them in order. +function pickConfirm(words, index = Math.min(4, words.length - 1)) { + const right = words[index]; + const others = [...new Set(words.filter((w) => w !== right))]; + for (let i = others.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [others[i], others[j]] = [others[j], others[i]]; } + const options = [right, ...others.slice(0, 3)]; + for (let i = options.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [options[i], options[j]] = [options[j], options[i]]; } + return { index, options, wrong: [] }; } // The BYO-entropy page, shared by Create (re-derives the draft phrase) and @@ -1930,7 +1940,7 @@ function signInAnother() { clearSeedDrafts(); ui.fromWallet = true; ui.unlockError = ''; ui.screen = 'unlock'; // Starts on Create new (its Generate button), not the seed-import box. - ui.unlockTab = 'create'; ui.createStep = 'gen'; ui.draftMnemonic = ''; ui.confirm = []; + ui.unlockTab = 'create'; ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.confirm = []; ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; render(); } @@ -2803,7 +2813,7 @@ function lock({ offerPassword = false } = {}) { ui.confirmRemove = null; ui.editId = null; ui.editLabel = ''; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.importText = ''; ui.entropyPage = null; @@ -3385,7 +3395,7 @@ function goHome() { ui.sendError = ''; ui.fromWallet = false; } else if ((ui.onb && ui.onb.step !== 'welcome') - || ui.draftMnemonic || ui.createStep !== 'gen' || ui.importText) { + || ui.draftMnemonic || !['intro', 'gen'].includes(ui.createStep) || ui.importText) { // Mid-create with no wallet minted yet (wizard step, a generated seed on // screen, the confirm quiz, a pasted import): the logo backs out to the // start page this began from — not the vault prompt, which reads as a @@ -3395,7 +3405,7 @@ function goHome() { ui.nostrLoginOpen = false; ui.screen = 'unlock'; ui.unlockTab = 'create'; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; clearSeedDrafts(); ui.entropyPage = null; @@ -3414,7 +3424,7 @@ function goHome() { } else { ui.screen = 'unlock'; ui.unlockTab = 'create'; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.confirm = []; ui.unlockError = ''; @@ -3494,7 +3504,7 @@ function accountsScreen() { if (aw.from === 'new') { ui.newWalletKind = kind; ui.addWallet = null; - ui.draftMnemonic = ''; ui.createStep = 'gen'; ui.confirm = []; ui.passphrase = ''; ui.confirmPass = ''; ui.passOpen = false; + ui.draftMnemonic = ''; ui.createStep = 'intro'; ui.confirm = []; ui.passphrase = ''; ui.confirmPass = ''; ui.passOpen = false; ui.showPass = false; ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = true; ui.unlockError = ''; render(); } else { @@ -4190,7 +4200,7 @@ function onboardScreen() { const startCreate = () => { ui.unlockTab = 'create'; ui.unlockError = ''; - ui.createStep = 'gen'; + ui.createStep = 'intro'; if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic(); o.step = 'seed'; render(); @@ -4292,26 +4302,36 @@ function onboardScreen() { ui.tab = 'receive'; render(); }; + // Later means later, not never: seedSeen stays false, so the wallet + // keeps a quiet reminder until the phrase has actually been seen. + // Except on the way to a coinos.io move: the balance about to arrive + // must have its twelve words written down first, so Later isn't offered. + const later = migrateLinkPending() ? null : h('button', { class: 'btn-ghost btn-block', onClick: () => { + if (canOfferSpending()) { o.step = 'spend'; o.enterAddr = undefined; render(); return; } + ui.onb = null; + try { localStorage.removeItem(ONB_STEP_KEY); } catch {} + ui.account = 'savings'; + try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {} + ui.tab = 'receive'; + render(); + } }, t('onbBackupLater')); + // a heads-up first: the next screen shows the secret (Adam, 2026-11-07) + if (!o.backupShown) { + return page([ + title(t('onbBackupTitle')), + h('p', { class: 'muted', style: 'margin:0' }, t('onbBackupBody')), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { id: 'onb-backup-show', class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => { o.backupShown = true; render(); } }, t('onbBackupShow')), + later, + ]); + } return page([ title(t('onbBackupTitle')), - h('p', { class: 'muted', style: 'margin:0' }, t('onbBackupBody')), h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w)))), h('div', { class: 'row gap6' }, copyBtn(acc.mnemonic, t('copyPhrase'))), h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: done }, t('onbBackupDone')), - // Later means later, not never: seedSeen stays false, so the wallet - // keeps a quiet reminder until the phrase has actually been seen. - // Except on the way to a coinos.io move: the balance about to arrive - // must have its twelve words written down first, so Later isn't offered. - migrateLinkPending() ? null : h('button', { class: 'btn-ghost btn-block', onClick: () => { - if (canOfferSpending()) { o.step = 'spend'; o.enterAddr = undefined; render(); return; } - ui.onb = null; - try { localStorage.removeItem(ONB_STEP_KEY); } catch {} - ui.account = 'savings'; - try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {} - ui.tab = 'receive'; - render(); - } }, t('onbBackupLater')), + later, ]); } if (o.step === 'seed') { diff --git a/src/features/gifts.js b/src/features/gifts.js index 42e794d..f23ecc8 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -288,51 +288,47 @@ export function giftsFeature(ctx) { h('h2', { style: 'margin:0' }, claimed ? t('claimedTitle') : t('newWalletTitle')), h('p', { class: 'muted', style: 'margin:0' }, claimed ? t('claimedBody') : t('newWalletBody')) ), - h('div', { class: 'card col' }, - h('h3', {}, t('recoveryPhrase')), - h('div', { class: 'warn-box' }, t('writeDownWarn')), - h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, - words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w))) - ), - h('div', { class: 'row gap6' }, copyBtn(wallet.mnemonic, t('copyPhrase'))), - h('button', { class: 'btn-primary btn-block', onClick: () => { ui.confirm = pickConfirm(words); ui.claimError = ''; ui.claimStep = 'verify'; render(); } }, t('verifyBackup')), - h('button', { class: 'btn-block', onClick: finishClaim }, t('skipVerification')) - ) + // a heads-up before the words, then the words, then one word to + // check (Adam, 2026-11-07) — the same three steps as a new wallet + !ui.claimShown + ? h('div', { class: 'card col' }, + h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), + h('p', { style: 'margin:0' }, t('seedIntroBody', { n: words.length })), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { id: 'claim-seed-show', class: 'btn-primary btn-block', onClick: () => { ui.claimShown = true; render(); } }, t('seedIntroShow'))) + : h('div', { class: 'card col' }, + h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), + h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, + words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w))) + ), + h('div', { class: 'row gap6' }, copyBtn(wallet.mnemonic, t('copyPhrase'))), + h('button', { id: 'claim-seed-next', class: 'btn-primary btn-block', onClick: () => { ui.confirm = pickConfirm(words); ui.claimError = ''; ui.claimStep = 'verify'; render(); } }, t('next'))) ); } if (ui.claimStep === 'verify') { - // Same word-confirmation as new-wallet creation: prove the recipient wrote - // the phrase down before sending them off into their freshly funded wallet. const words = wallet.mnemonic.split(' '); + const q = ui.confirm; + if (!q || !q.options) { ui.claimStep = 'backup'; ui.claimShown = true; return claimScreen(); } return h( 'div', { class: 'col', style: 'gap:16px' }, brandHeader(false), h('div', { class: 'card col' }, h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), - h('p', { class: 'muted', style: 'margin:0' }, t('confirmBackupIntro')), - ...ui.confirm.map((c, i) => - h('label', { class: 'field' }, - h('span', { class: 'lab' }, t('wordN', { n: c.index + 1 })), - h('input', { - type: 'text', class: 'mono-input', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', - value: c.value, onInput: (e) => (ui.confirm[i].value = e.target.value.trim()), - }) - ) - ), - ui.claimError && h('div', { class: 'notice err' }, ui.claimError), - h('div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.claimStep = 'backup'; ui.claimError = ''; render(); } }, t('back')), + h('p', { style: 'margin:0' }, t('quizPrompt', { n: q.index + 1 })), + h('div', { class: 'quiz-opts' }, q.options.map((w) => h('button', { - class: 'btn-primary grow', + type: 'button', + class: 'quiz-opt' + (q.wrong.includes(w) ? ' wrong' : ''), onClick: () => { - const ok = ui.confirm.every((c) => c.value.toLowerCase() === words[c.index]); - if (!ok) { ui.claimError = t('wordsMismatch'); render(); return; } - finishClaim(); + if (w === words[q.index]) { ui.claimError = ''; finishClaim(); return; } + if (!q.wrong.includes(w)) q.wrong.push(w); + ui.claimError = t('quizWrong'); render(); }, - }, t('openWallet')) - ), - h('button', { class: 'btn-block', onClick: finishClaim }, t('skipVerification')) + }, w))), + ui.claimError && h('div', { class: 'notice err' }, ui.claimError), + h('button', { id: 'claim-seed-skip', class: 'btn-ghost btn-block', onClick: () => { ui.claimError = ''; finishClaim(); } }, t('skip')), + h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { ui.claimStep = 'backup'; ui.claimShown = true; ui.claimError = ''; render(); } }, t('quizBack')), ) ); } @@ -366,7 +362,7 @@ export function giftsFeature(ctx) { // view the claim on a block explorer, or head back to the home screen. h('button', { class: 'btn-primary btn-block', onClick: () => { ui.claimTaken = null; ui.claimedAmount = 0; - if (activeAccount() && activeAccount().provisional) { ui.claimStep = 'backup'; commitAccount(); } + if (activeAccount() && activeAccount().provisional) { ui.claimStep = 'backup'; ui.claimShown = false; commitAccount(); } else { ui.screen = 'wallet'; ui.claimStep = null; landAccount(); } render(); } }, activeAccount() && activeAccount().provisional ? t('createWalletAnyway') : t('goToWallet')), @@ -488,7 +484,7 @@ export function giftsFeature(ctx) { commitAccount(); // claimed — keep a fresh gift wallet (no-op for an existing one) if (ui.claimFrom) hook('followGiftSender', ui.claimFrom); if (wasProvisional) { - ui.claimStep = 'backup'; // fresh wallet — show the new seed to back up + ui.claimStep = 'backup'; ui.claimShown = false; // fresh wallet — show the new seed to back up } else { ui.screen = 'wallet'; // existing wallet — straight into it, no new seed to back up ui.claimStep = null; diff --git a/src/i18n.js b/src/i18n.js index e45fa49..369deb2 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -197,6 +197,15 @@ const STR = { skipVerification: 'Skip verification', confirmBackupIntro: 'Confirm your backup by entering the requested words.', wordN: 'Word #{n}', + seedIntroBody: 'Next you’ll see your seed phrase: {n} words that are your wallet. Write them down on paper, in order, and keep them somewhere safe. They’re the only way to get your coins back if you lose this device.', + 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', + skip: 'Skip', + quizPrompt: 'Which one is word #{n}?', + quizWrong: 'Not that one. Check what you wrote down.', + quizBack: 'Show the words again', + onbBackupShow: 'Show seed phrase', reenterPassphrase: 'Re-enter passphrase', back: '← Back', cancel: 'Cancel', diff --git a/src/style.css b/src/style.css index dfe9b9d..d753604 100644 --- a/src/style.css +++ b/src/style.css @@ -344,6 +344,10 @@ textarea.send-dest::placeholder { font-family: var(--sans); } eight-letter word ("resemble") pushed the third column out past the card's padding. The cells are a touch tighter so every BIP-39 word (eight letters at most) still fits whole in a third of a narrow card. */ +/* The one-word check after the phrase: four of its words, tap the right one. */ +.quiz-opts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } +.quiz-opt { font-family: var(--mono); font-size: 16px; padding: 14px 8px; } +.quiz-opt.wrong { border-color: var(--red); color: var(--red); background: var(--red-soft); } .words { display: grid; grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .words .w { display: flex; align-items: baseline; gap: 5px; background: var(--surface2); min-width: 0; diff --git a/tools/gift-profile-setup-test.js b/tools/gift-profile-setup-test.js index 9304d1a..04e58c5 100644 --- a/tools/gift-profile-setup-test.js +++ b/tools/gift-profile-setup-test.js @@ -53,7 +53,11 @@ try { page.on('pageerror', (e) => errors.push(String(e))); await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 }); await toSeed(page, 'Happy birthday! 🎂 <b>not bold</b>'); - await click(page, 'skip verification'); + check('a heads-up comes before the words', /no one is looking/i.test(await text(page)) && await page.evaluate(() => !document.querySelector('.words'))); + await click(page, 'show seed phrase'); + check('...then the words, with Next', await page.evaluate(() => document.querySelectorAll('.words .w').length === 12) && await click(page, 'next')); + check('...then one word to pick, or Skip', await waitText(page, /which one is word #5/i, 5000) && await page.evaluate(() => document.querySelectorAll('.quiz-opt').length === 4)); + await click(page, 'skip'); check('the seed done: the wizard asks for a username', await waitText(page, /now pick a username/i, 10000), (await text(page)).slice(0, 200)); check('...with the claim form (or its wait) and Not now', await page.evaluate(() => !!document.querySelector('.onb')) && /not now/i.test(await text(page))); if (process.argv[2]) await page.screenshot({ path: process.argv[2] }); @@ -66,30 +70,25 @@ try { p2.on('pageerror', (e) => errors.push(String(e))); await p2.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 }); await toSeed(p2); - check('Verify backup opens the word check', await click(p2, 'verify') && await waitText(p2, /word #?\d/i, 5000)); - await sleep(500); - // the verify page doesn't show the words: read them from the account - const filled = await p2.evaluate(() => { - const accs = JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}'); - const list = Array.isArray(accs) ? accs : accs.accounts || accs.list || []; - const m = list.map((a) => a.mnemonic).filter(Boolean).pop(); if (!m) return false; - const w = m.split(' '); - for (const lab of document.querySelectorAll('label.field')) { - const n = Number(lab.querySelector('.lab').textContent.match(/\d+/)[0]); - const inp = lab.querySelector('input'); - const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; - set.call(inp, w[n - 1]); inp.dispatchEvent(new Event('input', { bubbles: true })); - } - return true; - }); + await click(p2, 'show seed phrase'); + const fifth = await p2.evaluate(() => document.querySelectorAll('.words .w .t')[4]?.textContent.trim()); + check('Next opens the word check', await click(p2, 'next') && await waitText(p2, /which one is word #5/i, 5000)); + // a wrong pick says so and stays + const wrongOk = await p2.evaluate((right) => { + const b = [...document.querySelectorAll('.quiz-opt')].find((x) => x.textContent.trim() !== right); b?.click(); return !!b; + }, fifth); + await sleep(300); + check('...a wrong word is marked, and the check stays', wrongOk && await p2.evaluate(() => !!document.querySelector('.quiz-opt.wrong')) && /not that one/i.test(await text(p2))); + const filled = await p2.evaluate((right) => { + const b = [...document.querySelectorAll('.quiz-opt')].find((x) => x.textContent.trim() === right); b?.click(); return !!b; + }, fifth); if (filled) { - await click(p2, 'open wallet'); check('a verified seed leads to the username step too', await waitText(p2, /now pick a username/i, 10000)); await click(p2, 'not now'); let gone = false; for (let i = 0; i < 20 && !gone; i++) { await sleep(250); gone = !(await p2.evaluate(() => !!document.querySelector('.onb'))); } check('Not now: into the wallet, the wizard gone', gone, (await text(p2)).slice(0, 200)); - } else console.log(' (accounts not in localStorage — verify path not driven)'); + } else check('the right word is among the choices', false); check('no page errors', errors.length === 0, errors.join(' | ')); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ a gift\'s new wallet is offered a name, a picture and a cover' : '\n❌ failed'); diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js index 3eefb02..8fc1071 100644 --- a/tools/onboard-backup-test.js +++ b/tools/onboard-backup-test.js @@ -87,8 +87,11 @@ try { // words on screen at once, with what they are. await page.evaluate(() => document.getElementById('onb-create')?.click()); await sleep(600); - // the warning box that said "write these 12 words down" is gone - // (2026-11-06): the words themselves are the explanation + // a heads-up first, no warning icon (2026-11-07): sensitive words next + const intro = await page.evaluate(() => ({ words: document.querySelectorAll('.words .w').length, text: document.body.innerText, warn: !!document.querySelector('.warn-box') })); + check('the words wait behind a heads-up to write them down', intro.words === 0 && /write them down/i.test(intro.text) && !intro.warn && !/⚠/.test(intro.text)); + await page.evaluate(() => document.getElementById('seed-intro-next')?.click()); + await sleep(400); const made = await page.evaluate(() => document.querySelectorAll('.words .w .t').length); check('the way in shows what the wallet actually is: its twelve words', made === 12, made + ' words'); await page.close(); @@ -135,6 +138,9 @@ try { { const { page, ctx, seed } = await signIn(); check('a sign-in is asked to write the phrase down', await waitText(page, 'write down your recovery phrase')); + check('...with the words behind Show seed phrase', await page.evaluate(() => !document.querySelector('.words'))); + await click(page, 'show seed phrase'); + await sleep(300); const shown = await page.evaluate(() => [...document.querySelectorAll('.words .w .t')].map((e) => e.textContent.trim())); check('...and the real twelve words are on screen', shown.length === 12 && shown.join(' ') === seed, shown.length + ' words' + (shown.join(' ') === seed ? ', matching the derived seed' : ', WRONG')); |
