diff options
| -rw-r--r-- | src/app.js | 41 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | tools/signup-quick-test.js | 28 |
3 files changed, 65 insertions, 5 deletions
@@ -2199,6 +2199,7 @@ function loadWatchAccounts() { // a sign-up's Spending-only shape and its ways back in outlive the session ...(d.spendingOnly ? { kind: 'spending', savingsLater: true } : {}), ...(d.signupAt ? { signupAt: d.signupAt } : {}), + ...(d.waysBackPrompted ? { waysBackPrompted: d.waysBackPrompted } : {}), ...(Array.isArray(d.recovery) && d.recovery.length ? { recovery: d.recovery } : {}) })); } catch { return []; } } @@ -2219,6 +2220,7 @@ function saveDirectory() { return { id: a.id, label: a.label, xpub: a.xpub, autoLock: a.autoLock || 0, network: a.network, nostrPk: a.nostrPk, seedSeen: !!a.seedSeen, ...(a.kind === 'spending' && a.savingsLater ? { spendingOnly: true } : {}), ...(a.signupAt ? { signupAt: a.signupAt } : {}), + ...(a.waysBackPrompted ? { waysBackPrompted: a.waysBackPrompted } : {}), ...(a.recovery && a.recovery.length ? { recovery: a.recovery } : {}) }; }); localStorage.setItem(WATCH_KEY, JSON.stringify(dir)); @@ -5061,10 +5063,43 @@ function addRecoveryMethod(m) { } wallet.registerCacheExtension({ mergeAlways: true, - save: () => { const r = recoveryMethods(); return r.length ? { recovery: r } : {}; }, - load: (d) => { if (d && d.recovery) mergeRecovery(d.recovery); }, + save: () => { + const a = activeAccount(), r = recoveryMethods(); + return { ...(r.length ? { recovery: r } : {}), ...(a && a.waysBackPrompted ? { waysBackPrompted: a.waysBackPrompted } : {}) }; + }, + load: (d) => { + if (!d) return; + if (d.recovery) mergeRecovery(d.recovery); + // asked on another device: asked (only ever turns on) + const a = activeAccount(); + if (d.waysBackPrompted && a && !a.waysBackPrompted) { a.waysBackPrompted = d.waysBackPrompted; persistAccounts(); } + }, }); +// A sign-up is asked for a way back in ONCE, and only when there's something +// to lose (Adam, 2026-11-15: "only prompt to add a way back in after there's +// some balance and only if we haven't prompted before"). The ask is recorded +// the moment it first shows — on the account, the directory and the synced +// snapshot — and stays up for that visit until Add one or Not now. +function waysBackPrompt(a) { + if (recoveryMethods(a).length) { if (ui.waysBackPromptFor === a.id) ui.waysBackPromptFor = null; return null; } + if (ui.waysBackPromptFor !== a.id) { + if (a.waysBackPrompted || !wallet.loaded) return null; + const bal = (featureHook('spendingSat') || 0) + (wallet.spendable || 0) + (wallet.pendingIncoming || 0); + if (bal <= 0) return null; + a.waysBackPrompted = Date.now(); + persistAccounts(); + try { wallet.saveCache(); } catch {} + ui.waysBackPromptFor = a.id; + } + const done = () => { ui.waysBackPromptFor = null; }; + return h('div', { class: 'notice info col ways-back-prompt', style: 'margin:12px 0 0;gap:10px' }, + h('span', {}, t('waysBackPrompt')), + h('div', { class: 'row gap6' }, + h('button', { class: 'btn-sm btn-primary', onClick: () => { done(); ui.tab = 'settings'; ui.settingsPage = 'wallet'; render(); } }, t('waysBackReminderGo')), + h('button', { class: 'btn-sm btn-ghost', onClick: () => { done(); render(); } }, t('onbNotNow')))); +} + // A wallet whose recovery phrase has never been on screen keeps one quiet // line about it. "Later" is a real answer, so this is a row and not a modal — // but it doesn't dismiss, because the risk doesn't go away on its own. It @@ -5076,7 +5111,7 @@ function backupNotice() { // A sign-up never saw a phrase and wasn't asked to: what it needs is A way // back in, any one — and once it has one, nothing to nag about. const signup = !!a.signupAt; - if (signup && recoveryMethods(a).length) return null; + if (signup) return waysBackPrompt(a); return h('div', { class: 'small muted', style: 'margin:10px 0 0;text-align:center' }, t(signup ? 'waysBackReminder' : 'backupReminder'), ' ', diff --git a/src/i18n.js b/src/i18n.js index 97bb066..bb82ed2 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1502,6 +1502,7 @@ const STR = { onbNameTitle: 'Claim your name', onbNameBody: "It's your username and your payment address: people pay you, follow you and message you with just your name.", waysBackReminder: 'Add a way back into your account, in case you lose this device.', + waysBackPrompt: 'You have bitcoin in here now. Add a way back into your account, in case you lose this device.', waysBackReminderGo: 'Add one', addSavings: 'Add a savings account', addSavingsDesc: 'On-chain bitcoin, for keeping. Same account, a second balance.', diff --git a/tools/signup-quick-test.js b/tools/signup-quick-test.js index d184457..a269faf 100644 --- a/tools/signup-quick-test.js +++ b/tools/signup-quick-test.js @@ -82,7 +82,32 @@ try { })); let s = await shape(); check('in, on Spending alone (no Savings face, no name on it)', s.faces === 1 && !s.carousel && !s.label, JSON.stringify({ faces: s.faces, carousel: s.carousel, label: s.label })); - check('a quiet line asks for a way back in', /add a way back into your account/i.test(s.body)); + // asked once, and only with money in the account (2026-11-15): not on an empty one + check('no way-back-in prompt on an empty account', !/add a way back into your account/i.test(s.body) && !(await page.$('.ways-back-prompt'))); + // money arrives in Spending (the ark feature, found on the heap, made to + // report a balance; then a repaint) + const giveBalance = async () => { + const protoH = await page.evaluateHandle(() => Object.prototype); + const all = await page.queryObjects(protoH); + const found = await page.evaluate((objs) => { + const f = objs.find((o) => { try { return o && o.id === 'ark' && typeof o.spendingSat === 'function'; } catch { return false; } }); + if (!f) return false; + f.spendingSat = () => 5000; + window.dispatchEvent(new Event('resize')); + document.querySelector('.tabs button')?.click(); document.querySelector('.tabs button')?.click(); + return true; + }, all); + await all.dispose(); await protoH.dispose(); + await sleep(800); + return found; + }; + check('(test: found the ark feature)', await giveBalance()); + check('with a balance, it asks once for a way back in', !!(await page.$('.ways-back-prompt')), (await text()).slice(0, 300).replace(/\n/g, ' | ')); + await page.evaluate(() => [...document.querySelectorAll('.ways-back-prompt button')].find((b) => /not now/i.test(b.textContent)).click()); await sleep(400); + check('Not now puts it away', !(await page.$('.ways-back-prompt'))); + await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.balance-face', { timeout: 20000 }); await sleep(1500); + await giveBalance(); + check('...and never again, not after a reload', !(await page.$('.ways-back-prompt'))); for (let i = 0; i < 60 && !(await page.$('#recv-amount')); i++) await sleep(250); check('Receive has an Amount button', !!(await page.$('#recv-amount'))); check('no History heading on an empty wallet', !/\bHISTORY\b/.test(await page.evaluate(() => document.body.innerText))); @@ -110,7 +135,6 @@ try { const pk = await page.evaluate(() => ({ rows: [...document.querySelectorAll('.ways-back-row')].map((e) => e.dataset.way), err: document.querySelector('.ways-back .notice.err')?.textContent || '' })); check('adding a passkey lists it', pk.rows.includes('passkey') && !pk.err, JSON.stringify(pk)); const backups = await page.evaluate(() => (window.__relayGuard.events || []).filter((e) => e.kind === 30078 && e.tags.some((t) => t[0] === 'd' && t[1] === 'coinos:wallet:v1'))); - check('the reminder line is gone once there is a way back in', await (async () => { await page.goBack(); await page.goBack(); await sleep(600); const gone = !/add a way back into your account/i.test(await text()); await openWalletPage(); return gone; })()); // the list and the reminder, as the app sees them (record a method directly) check('the phrase card waits behind Pen and paper', !(await page.$('.recovery-card'))); await page.evaluate(() => document.querySelector('.ways-back-add[data-way="phrase"]')?.click()); |
