aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-15 07:34:03 -0700
committerAdam Soltys <[email protected]>2026-06-15 07:34:03 -0700
commit249b1239bab190e6cc61f5673204134cf95d13db (patch)
treec556a396dbd332f1f01da948712d12b772a5cbbb /src
parentfe7d3b147c8d079d9c1cec76847f3941340cfebf (diff)
downloadcoinosv3-249b1239bab190e6cc61f5673204134cf95d13db.tar.gz
coinosv3-249b1239bab190e6cc61f5673204134cf95d13db.zip
Gate recovery-phrase reveal behind a warning + Proceed step
Opening Backup now shows the warning with the 12 words masked (••••••) and a Proceed/Cancel choice. The real words and passphrase are kept out of the DOM until Proceed, so the user reads the "make sure nobody is watching" warning before anything sensitive renders. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js42
-rw-r--r--src/style.css1
2 files changed, 30 insertions, 13 deletions
diff --git a/src/app.js b/src/app.js
index 48edf7d..6e98cac 100644
--- a/src/app.js
+++ b/src/app.js
@@ -29,7 +29,8 @@ const ui = {
importText: '',
passphrase: '',
showPass: false,
- reveal: false, // showing the recovery-phrase modal
+ reveal: false, // recovery-phrase modal open
+ revealShown: false, // phrase actually unmasked (after reading the warning)
offlineFallback: false, // auto-entered offline because the network was unreachable
unlockError: '',
@@ -396,6 +397,7 @@ function lock() {
ui.passphrase = '';
ui.confirm = [];
ui.reveal = false;
+ ui.revealShown = false;
render();
}
@@ -420,7 +422,7 @@ function brandHeader(withLock) {
{ class: `badge dot ${wallet.offline ? 'off' : ''} ${wallet.live ? 'live' : ''}` },
status
),
- withLock && h('button', { class: 'btn-sm', title: 'Show recovery phrase', onClick: () => { ui.reveal = true; render(); } }, 'Backup'),
+ withLock && h('button', { class: 'btn-sm', title: 'Show recovery phrase', onClick: () => { ui.reveal = true; ui.revealShown = false; render(); } }, 'Backup'),
withLock && h('button', { class: 'btn-sm', onClick: lock }, 'Logout')
)
);
@@ -429,8 +431,19 @@ function brandHeader(withLock) {
// Modal to view the recovery phrase (+ passphrase) again — important for users
// who skipped backup verification.
function revealModal() {
+ const shown = ui.revealShown;
const words = wallet.mnemonic.split(' ');
- const close = () => { ui.reveal = false; render(); };
+ const close = () => { ui.reveal = false; ui.revealShown = false; render(); };
+
+ // Until "Proceed", the real words are never put in the DOM — show 12 masked
+ // placeholders so the warning is read first.
+ const cells = words.map((w, i) =>
+ h('div', { class: 'w' + (shown ? '' : ' masked') },
+ h('span', { class: 'n' }, i + 1),
+ h('span', { class: 't' }, shown ? w : '••••••')
+ )
+ );
+
return h(
'div',
{ class: 'overlay', onClick: (e) => { if (e.target.classList.contains('overlay')) close(); } },
@@ -438,21 +451,24 @@ function revealModal() {
'div',
{ class: 'modal card col' },
h('h3', {}, 'Recovery phrase'),
- h('div', { class: 'warn-box' }, '⚠ Anyone who can see these words can steal your funds. Make sure nobody is watching.'),
- h('div', { class: 'words' },
- words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w)))
- ),
- wallet.passphrase
+ h('div', { class: 'warn-box' }, '⚠ Anyone who can see these words can steal your funds. Make sure nobody is watching before you reveal them.'),
+ h('div', { class: 'words' }, cells),
+ shown && wallet.passphrase
? h('div', { class: 'col gap6' },
h('span', { class: 'lab' }, 'BIP39 passphrase'),
h('div', { class: 'addr-box' }, wallet.passphrase)
)
: null,
- h('div', { class: 'row gap6 wrap' },
- copyBtn(wallet.mnemonic, 'Copy phrase'),
- wallet.passphrase ? copyBtn(wallet.passphrase, 'Copy passphrase') : null,
- h('button', { class: 'btn-primary grow', onClick: close }, 'Done')
- )
+ shown
+ ? h('div', { class: 'row gap6 wrap' },
+ copyBtn(wallet.mnemonic, 'Copy phrase'),
+ wallet.passphrase ? copyBtn(wallet.passphrase, 'Copy passphrase') : null,
+ h('button', { class: 'btn-primary grow', onClick: close }, 'Done')
+ )
+ : h('div', { class: 'row gap6' },
+ h('button', { class: 'btn-ghost', onClick: close }, 'Cancel'),
+ h('button', { class: 'btn-primary grow', onClick: () => { ui.revealShown = true; render(); } }, 'Proceed')
+ )
)
);
}
diff --git a/src/style.css b/src/style.css
index 4e0a0e2..69c26b2 100644
--- a/src/style.css
+++ b/src/style.css
@@ -211,6 +211,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si
background: rgba(20, 23, 26, 0.5); display: grid; place-items: center;
}
.modal { max-width: 480px; width: 100%; max-height: 90vh; overflow: auto; }
+.words .w.masked .t { color: var(--faint); letter-spacing: 2px; }
.divider { height: 1px; background: var(--line); margin: 16px 0; border: 0; }
hr.divider { margin: 16px 0; }
.linklike { background: none; border: 0; padding: 0; color: var(--accent-ink); font-weight: 600; cursor: pointer; }