diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 13:51:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 13:51:40 -0700 |
| commit | def5985a29d6c589d54a5c12a5bfb34631f03259 (patch) | |
| tree | 338304c359a3d6a033b5479ee8ce4a6780111366 /src | |
| parent | ac3e0bf43aac82e66010c3e845556ab32ad03aed (diff) | |
| download | coinosv3-def5985a29d6c589d54a5c12a5bfb34631f03259.tar.gz coinosv3-def5985a29d6c589d54a5c12a5bfb34631f03259.zip | |
Save wallets by default; reveal the recovery phrase in two steps
A seed that only lives in this tab is one refresh from gone, and that
surprise costs people money — so a wallet now saves itself to the device
when it's opened or committed, no button required. No password is set
(Settings still offers one), and a locked existing vault is left to the
manual Save button since writing it needs their password.
The recovery card gains a middle step: the first click opens the grid
with the words masked — the warning and the shape of what's coming are
on screen before anything readable is — and a second click shows them.
Copy only appears once they're visible.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 46 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
2 files changed, 38 insertions, 9 deletions
@@ -74,7 +74,7 @@ const ui = { importText: '', passphrase: '', showPass: false, - revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning) + revealShown: false, // recovery phrase: false | 'masked' (grid, dots) | 'words' pubkeyShown: false, // account public key revealed in Settings giftMode: false, // gift sub-view active on the Send page giftAmount: '', // gift-create amount input @@ -850,6 +850,7 @@ async function activateAccount(acc, opts = {}) { // balance/history and re-enter the seed to spend again. if (acc.type !== 'watch' && !acc.provisional) acc.xpub = wallet.accountXpub(); persistAccounts(); + autoSave(acc); for (const f of FEATURES) { try { f.init && f.init(); } catch {} } // per-feature wallet lifecycle const hadCache = wallet.restoreCache(); // show last-known balance/history instantly // An opened gift link starts on a claim/back-up screen instead of the wallet. @@ -1001,6 +1002,7 @@ function commitAccount() { if (a.type !== 'watch') a.xpub = wallet.accountXpub(); // now eligible for the durable directory persistAccounts(); } + autoSave(a); } function removeAccount(id) { @@ -1098,6 +1100,22 @@ function mergeVaultList(list) { } } +// Keeping a wallet is the default: a seed that only lives in this tab is one +// refresh away from being gone, and that surprise costs people money. So a +// wallet saves itself to the device unless we can't write the vault — when +// one exists but is locked, the manual Save button still covers it. No +// password is set here; Settings can add one later. +function autoSave(acc) { + if (!acc || acc.type !== 'full' || acc.persisted || acc.provisional) return; + if (vaultPassword == null) { + if (hasVault()) return; // locked vault: needs their password, leave it manual + vaultPassword = ''; + } + acc.persisted = true; + writeVault(); + persistAccounts(); +} + // Toggle persistence on a full account. Prompts for the vault password when it // isn't unlocked this session ('set' the first time, 'enter' if a vault exists). function startSave(id) { @@ -1882,24 +1900,34 @@ function recoveryCard(a) { } if (!a.mnemonic) return h('div', { class: 'card col' }, h('h3', {}, t('importedKey')), h('p', { class: 'small muted', style: 'margin:0' }, t('importedKeyNote'))); - // Collapsed by default — the word grid only exists after an explicit reveal. + // Two steps on purpose. The first opens the card, with the words masked — + // so the grid is on screen and you can see how much of it there is (and get + // a warning) before anything readable is over your shoulder. The second + // actually shows them. if (!shown) return h('div', { class: 'card col' }, h('h3', {}, t('recoveryPhrase')), h('p', { class: 'small muted', style: 'margin:0' }, t('recoveryDesc')), - h('button', { class: 'btn-block', onClick: () => { ui.revealShown = true; render(); } }, t('revealRecovery'))); + h('button', { class: 'btn-block', onClick: () => { ui.revealShown = 'masked'; render(); } }, t('revealRecovery'))); + const unmasked = shown === 'words'; return h('div', { class: 'card col' }, h('h3', {}, t('recoveryPhrase')), h('div', { class: 'warn-box' }, t('recoveryWarn')), h('div', { class: 'words' }, words.map((w, i) => - h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w)))), - a.passphrase + h('div', { class: 'w' + (unmasked ? '' : ' masked') }, + h('span', { class: 'n' }, i + 1), + h('span', { class: 't' }, unmasked ? w : '••••••')))), + unmasked && a.passphrase ? h('div', { class: 'col gap6' }, h('span', { class: 'lab' }, t('bip39Passphrase')), h('div', { class: 'addr-box' }, a.passphrase)) : null, - h('div', { class: 'row gap6 wrap' }, - copyBtn(a.mnemonic, t('copyPhrase')), - a.passphrase ? copyBtn(a.passphrase, t('copyPassphrase')) : null, - h('button', { class: 'btn-sm grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide'))) + unmasked + ? h('div', { class: 'row gap6 wrap' }, + copyBtn(a.mnemonic, t('copyPhrase')), + a.passphrase ? copyBtn(a.passphrase, t('copyPassphrase')) : null, + h('button', { class: 'btn-sm grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide'))) + : h('div', { class: 'row gap6' }, + h('button', { class: 'btn-primary grow', onClick: () => { ui.revealShown = 'words'; render(); } }, t('revealWords')), + h('button', { class: 'btn-sm', onClick: () => { ui.revealShown = false; render(); } }, t('hide'))) ); } diff --git a/src/i18n.js b/src/i18n.js index 4d305d8..18fe2cd 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -363,6 +363,7 @@ const STR = { bip39Passphrase: 'Passphrase', copyPassphrase: 'Copy passphrase', revealRecovery: 'Reveal recovery phrase', + revealWords: 'Show the words', offlineTransfer: 'Offline transfer', offlineTransferDesc: 'Export your coins on an online device, then import the file on an offline device to sign without internet.', exportSnapshot: '⤓ Export snapshot', |
