aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js14
-rw-r--r--src/features/nostrlogin.js16
-rw-r--r--src/i18n.js7
-rw-r--r--tools/signup-quick-test.js6
4 files changed, 27 insertions, 16 deletions
diff --git a/src/app.js b/src/app.js
index cead211..f3c2dcb 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3247,7 +3247,10 @@ function settingsTab() {
// matters if the phone is lost tonight
case 'wallet':
if (ui.delAcct && a) return deleteAccountView(a);
- return page(a ? [featureHook('waysBackCard'), recoveryCard(a), balanceSettingsCard(a), walletNameCard(a), pubkeyCard(a), historyExportCard(), deleteAccountCard(a)] : []);
+ // the phrase is one of the ways back in: its card shows only once
+ // Pen and paper asks for it (accounts without the card keep it always)
+ { const ways = a ? featureHook('waysBackCard') : null;
+ return page(a ? [ways, ways && !ui.revealShown ? null : recoveryCard(a), balanceSettingsCard(a), walletNameCard(a), pubkeyCard(a), historyExportCard(), deleteAccountCard(a)] : []); }
case 'payments': return page(featureAll('settingsCards').reverse());
case 'pos': return page(featureAll('posSettingsCards').reverse()); // the till's own card first, then the terminal's
case 'network': return page([networkCard(), explorerCard()]); // an old bookmark of the tile; the cards live under Advanced now
@@ -3907,12 +3910,12 @@ function recoveryCard(a) {
// a warning) before anything readable is over your shoulder. The second
// actually shows them.
if (!shown)
- return h('div', { class: 'card col' },
+ return h('div', { class: 'card col recovery-card' },
h('h3', {}, t('recoveryPhrase')),
h('p', { class: 'small muted', style: 'margin:0' }, t('recoveryDesc')),
h('button', { class: 'btn-block', onClick: () => { ui.revealShown = 'masked'; render(); } }, t('revealRecovery')));
const unmasked = shown === 'words';
- return h('div', { class: 'card col' },
+ return h('div', { class: 'card col recovery-card' },
h('h3', {}, t('recoveryPhrase')),
h('div', { class: 'warn-box' }, t('recoveryWarn')),
h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, words.map((w, i) =>
@@ -7021,7 +7024,10 @@ const ctx = {
recoveryMethods: () => recoveryMethods(), addRecoveryMethod,
spendingOnlyAccount: () => { const a = activeAccount(); return !!(a && a.kind === 'spending' && a.savingsLater); },
seedSeen: () => !!(activeAccount() || {}).seedSeen,
- showRecoveryPhrase: () => { ui.tab = 'settings'; ui.settingsPage = 'wallet'; ui.revealShown = 'masked'; render(); },
+ showRecoveryPhrase: () => {
+ ui.tab = 'settings'; ui.settingsPage = 'wallet'; ui.revealShown = ui.revealShown || 'masked'; render();
+ requestAnimationFrame(() => { try { document.querySelector('.recovery-card')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch {} });
+ },
// Open (or create) a wallet from a mnemonic β€” used by nostr login.
openMnemonic: async (mnemonic, passphrase, opts) => enterWallet(mnemonic, passphrase, opts),
// Profile pictures retain the metadata-scrubbing image uploader. Public
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index e48e9a2..6f700b1 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -750,6 +750,8 @@ export function nostrLoginFeature(ctx) {
render();
}
}
+ // pen over a sheet of paper, in the line style of the passkey mark
+ const PAPER_MARK = '<svg width="19" height="19" style="display:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M13 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/><path d="M8 9h4"/><path d="M8 13h3"/><path d="M8 17h6"/><path d="M18.4 2.6a2 2 0 0 1 2.8 2.8L14 12.6l-3.6.9.9-3.6z"/></svg>';
const fmtDay = (ms) => { try { return new Date(ms).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } catch { return ''; } };
function waysBackCard() {
if (wallet.watchOnly || !wallet.mnemonic) return null;
@@ -780,13 +782,6 @@ export function nostrLoginFeature(ctx) {
else rows.push(done(OSTRICH, 'Nostr', (npubOf(m.id) || '').slice(0, 20) + '…', 'nostr'));
}
const seen = ctx.seedSeen();
- rows.push(h('div', { class: 'row gap6 ways-back-row', 'data-way': 'phrase', style: 'align-items:center;padding:8px 2px' },
- h('span', { style: 'display:flex;flex-shrink:0;width:23px;justify-content:center;font-size:17px' }, 'πŸ”‘'),
- h('span', { class: 'col', style: 'flex:1;gap:1px' },
- h('span', { style: 'font-weight:600' }, t('recoveryPhrase')),
- h('span', { class: 'small muted' }, t(seen ? 'waysBackPhraseSeen' : 'waysBackPhraseUnseen'))),
- seen ? h('span', { class: 'ways-back-check' }, 'βœ“')
- : h('button', { class: 'btn-sm', onClick: () => ctx.showRecoveryPhrase() }, t('waysBackShow'))));
const have = (via) => methods.some((m) => m.via === via) || (via !== 'nostr' && st.via === via);
const adds = [
hasPasskey && !have('passkey') ? add(PASSKEY_MARK, t('waysBackAddPasskey'), 'passkey', () => passkeyRecovery()) : null,
@@ -798,6 +793,13 @@ export function nostrLoginFeature(ctx) {
h('span', { class: 'linklike small', onClick: () => { ui.waysBackNostr = false; ui.waysBackError = ''; render(); } }, t('cancel'))),
signerButtons((make) => addRecovery(make, 'nostr'), { nostrOnly: true }))
: add(OSTRICH, t('waysBackAddNostr'), 'nostr', () => { ui.waysBackNostr = true; ui.waysBackError = ''; ui.nostrLoginError = ''; render(); }),
+ // the recovery phrase, as one more button (Adam, 2026-11-15): it opens
+ // the masked words below, ticked once they've been seen
+ h('button', { class: 'btn-block ways-back-add', 'data-way': 'phrase', disabled: !!busyVia, onClick: () => ctx.showRecoveryPhrase() },
+ h('span', { style: 'display:flex;align-items:center;justify-content:center;gap:8px' },
+ h('span', { style: 'display:flex;flex-shrink:0', html: PAPER_MARK }),
+ t('waysBackPaper'),
+ seen ? h('span', { class: 'ways-back-check', 'aria-label': t('waysBackOn') }, 'βœ“') : null)),
].filter(Boolean);
return h('div', { class: 'card col ways-back', style: 'gap:6px' },
h('h3', {}, t('waysBackTitle')),
diff --git a/src/i18n.js b/src/i18n.js
index 9b59710..c901576 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1515,9 +1515,10 @@ const STR = {
waysBackPhraseSeen: 'Seen. Keep it written down somewhere safe',
waysBackPhraseUnseen: 'Twelve words that open this account in any wallet',
waysBackShow: 'Show',
- waysBackAddPasskey: 'Add a passkey',
- waysBackAddGoogle: 'Add Google',
- waysBackAddNostr: 'Add a Nostr account',
+ waysBackAddPasskey: 'Passkey auth',
+ waysBackAddGoogle: 'Google auth',
+ waysBackAddNostr: 'Nostr auth',
+ waysBackPaper: 'Pen and paper',
waysBackAdded: 'Added. You can sign in with it now',
waysBackIsLogin: 'That is the account you signed in with; it already opens this one.',
waysBackTaken: 'That sign-in already opens a different coinos account, so it can’t open this one too.',
diff --git a/tools/signup-quick-test.js b/tools/signup-quick-test.js
index c80547d..a26a9ef 100644
--- a/tools/signup-quick-test.js
+++ b/tools/signup-quick-test.js
@@ -90,6 +90,7 @@ try {
await openWalletPage();
check('Settings β†’ Wallet opens with Ways back in', !!(await page.$('.ways-back')) && /ways back in/i.test(await text()));
const views = await page.$$eval('.balance-settings-row[data-view]', (l) => l.map((e) => e.dataset.view));
+ check('the ways back in read as auth buttons', (await page.$$eval('.ways-back-add', (l) => l.map((e) => e.textContent.trim()))).join('|') === 'Passkey auth|Google auth|Nostr auth|Pen and paper', (await page.$$eval('.ways-back-add', (l) => l.map((e) => e.textContent.trim()))).join('|'));
check('only Spending is listed, with Add a savings account', views.join() === 'spending' && !!(await page.$('#add-savings')), views.join());
if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.ways-back').scrollIntoView()); await page.screenshot({ path: process.env.SHOT }); }
// a passkey: the relays are stubbed, so the publish fails offline β€” the
@@ -101,9 +102,10 @@ try {
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)
- await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Show')?.click());
+ 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());
await sleep(500);
- check('Show opens the recovery phrase card', /recovery phrase/i.test(await text()));
+ check('Pen and paper opens the recovery phrase card', !!(await page.$('.recovery-card .words')));
await page.evaluate(() => document.getElementById('add-savings')?.click()); await sleep(800);
const views2 = await page.$$eval('.balance-settings-row[data-view]', (l) => l.map((e) => e.dataset.view));