diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 00:01:27 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 00:01:27 -0700 |
| commit | d8964bdf6f047d058731edfae6d07e57106661c8 (patch) | |
| tree | aa55f97ff440e87beec82b7d0e94bbe6762897a8 /tools | |
| parent | 5682cc88e54f00ce01fdd2ef01e5bb1a207b1be2 (diff) | |
| download | coinosv3-d8964bdf6f047d058731edfae6d07e57106661c8.tar.gz coinosv3-d8964bdf6f047d058731edfae6d07e57106661c8.zip | |
Offer a password the first time you lock
Locking with no password locks nothing — the wallets sit on the device
under an empty one and reopen with a tap. So the first deliberate lock
now asks whether to set one, explaining plainly what happens if you
don't. 'Not now' is a real answer, recorded, and never asked again: a
wallet that nags is a wallet people stop reading.
The offer rides the deliberate lock only. lock() is also where the idle
timer, the last-wallet-removed path and boot-after-expiry land, and a
password form appearing because you went to lunch is a different app
than the one you left.
Two subtleties worth naming: the offer is set after lock()'s ui reset
block, which clears ui.pw and had been swallowing it; and taking the
offer re-enters lock(), which now finds a password and lands on the
unlock screen instead of the wallet list.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/lock-wallet-test.js | 59 |
1 files changed, 59 insertions, 0 deletions
diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js index 4bc56b1..1c46f2b 100644 --- a/tools/lock-wallet-test.js +++ b/tools/lock-wallet-test.js @@ -50,6 +50,12 @@ try { console.log('\n[locking with no password set]'); await logout(); let txt = await body(); + check('it offers a password once', /protect this device/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); + check('and does not force one', /not now/i.test(txt)); + await click('button', 'Not now'); + await sleep(800); + txt = await body(); + check('declining locks anyway', !/protect this device/i.test(txt)); check('no password is demanded', !/enter your password|unlock saved wallets/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); check('the saved wallet is still there', (await vaultSize()) === savedBytes); check('and it is offered back', /wallet 1|wallets/i.test(txt), txt.slice(0, 140).replace(/\n+/g, ' | ')); @@ -57,6 +63,13 @@ try { const listed = await page.evaluate(() => [...document.querySelectorAll('button, .item, .acct')].map((e) => e.textContent.trim()).filter((x) => /Wallet 1/.test(x))); check('and only once — no phantom duplicate', listed.length === 1, JSON.stringify(listed)); + console.log('\n[the offer is not a nag]'); + await page.evaluate(() => { const e = [...document.querySelectorAll('*')].find((n) => n.children.length === 0 && /Wallet 1/.test(n.textContent) && n.closest('button, .item, .acct')); if (e) e.closest('button, .item, .acct').click(); }); + await sleep(2000); + await waitText('receive', 15000); + await logout(); + check('it does not ask a second time', !/protect this device/i.test(await body())); + console.log('\n[getting back in]'); await page.evaluate(() => { const e = [...document.querySelectorAll('*')].find((n) => n.children.length === 0 && /Wallet 1/.test(n.textContent) && n.closest('button, .item, .acct')); if (e) e.closest('button, .item, .acct').click(); }); await sleep(2000); @@ -97,6 +110,52 @@ try { const txt2 = await body(); check('it warns before wiping', /removes every wallet/i.test(txt2), txt2.slice(0, 140).replace(/\n+/g, ' | ')); check('and it has not wiped anything yet', (await vaultSize()) > 0); + // A clean profile, because the offer is once-per-device and the run above + // already answered it. + console.log('\n[taking the offer, on a fresh device]'); + const ctx = await browser.createBrowserContext(); + const p2 = await ctx.newPage(); + const body2 = () => p2.evaluate(() => document.body.innerText); + const click2 = (sel, x) => p2.evaluate((s, t) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(t.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, x); + const wait2 = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await body2()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; + await p2.goto('http://localhost:5235/', { waitUntil: 'domcontentloaded' }); + await p2.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await p2.reload({ waitUntil: 'domcontentloaded' }); + await sleep(400); + await click2('button', 'I already have a wallet'); + await sleep(300); + await click2('button', 'Import existing'); + await sleep(300); + await p2.waitForSelector('textarea'); + await p2.type('textarea', generateMnemonic(wordlist)); + await click2('button', 'Open wallet'); + await wait2('receive', 15000); + await sleep(2500); + await p2.evaluate(() => document.querySelector('.header-avatar')?.click()); + await sleep(400); + await click2('.icon-select-item', 'Lock wallet'); + await sleep(1200); + check('offered on this device too', /protect this device/i.test(await body2())); + + const f2 = await p2.$$('input[type=password]'); + check('a new password and a confirmation', f2.length === 2, `${f2.length} field(s)`); + await f2[0].type('correct horse'); + await f2[1].type('correct horse'); + await click2('button', 'Save'); + await sleep(1500); + check('accepting it locks for real', /unlock saved wallets/i.test(await body2()), (await body2()).slice(0, 110).replace(/\n+/g, ' | ')); + + const wrong = await p2.$('input[type=password]'); + await wrong.type('not the password'); + await click2('button', 'Unlock'); + await sleep(800); + check('the wrong password is refused', /wrong password/i.test(await body2())); + await p2.evaluate(() => { const i = document.querySelector('input[type=password]'); if (i) { i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); } }); + const right = await p2.$('input[type=password]'); + await right.type('correct horse'); + await click2('button', 'Unlock'); + check('the right one opens it', await wait2('receive', 15000)); + await ctx.close(); } catch (e) { check('run completed', false, e.message); } finally { |
