aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 13:51:40 -0700
committerAdam Soltys <[email protected]>2026-08-03 13:51:40 -0700
commitdef5985a29d6c589d54a5c12a5bfb34631f03259 (patch)
tree338304c359a3d6a033b5479ee8ce4a6780111366 /src
parentac3e0bf43aac82e66010c3e845556ab32ad03aed (diff)
downloadcoinosv3-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.js46
-rw-r--r--src/i18n.js1
2 files changed, 38 insertions, 9 deletions
diff --git a/src/app.js b/src/app.js
index 011fba8..328e03b 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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',