aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-15 07:40:38 -0700
committerAdam Soltys <[email protected]>2026-06-15 07:40:38 -0700
commit06938ca188ca2fe3e8e7bf9431520273a684636a (patch)
treefc14925038f135b93790650cbda1283aba0565a6 /src
parent249b1239bab190e6cc61f5673204134cf95d13db (diff)
downloadcoinosv3-06938ca188ca2fe3e8e7bf9431520273a684636a.tar.gz
coinosv3-06938ca188ca2fe3e8e7bf9431520273a684636a.zip
Move Backup from header to its own tab (away from Logout)
Prevents accidental Logout clicks when reaching for Backup. Backup is now a tab rendering the gated recovery-phrase reveal inline; Logout is the only header button. The phrase re-masks whenever tabs change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js63
-rw-r--r--src/style.css5
2 files changed, 26 insertions, 42 deletions
diff --git a/src/app.js b/src/app.js
index 6e98cac..70c1f09 100644
--- a/src/app.js
+++ b/src/app.js
@@ -29,8 +29,7 @@ const ui = {
importText: '',
passphrase: '',
showPass: false,
- reveal: false, // recovery-phrase modal open
- revealShown: false, // phrase actually unmasked (after reading the warning)
+ revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning)
offlineFallback: false, // auto-entered offline because the network was unreachable
unlockError: '',
@@ -396,7 +395,6 @@ function lock() {
ui.importText = '';
ui.passphrase = '';
ui.confirm = [];
- ui.reveal = false;
ui.revealShown = false;
render();
}
@@ -422,21 +420,17 @@ 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; ui.revealShown = false; render(); } }, 'Backup'),
withLock && h('button', { class: 'btn-sm', onClick: lock }, 'Logout')
)
);
}
-// Modal to view the recovery phrase (+ passphrase) again — important for users
-// who skipped backup verification.
-function revealModal() {
+// Backup tab — view the recovery phrase (+ passphrase) again, important for
+// users who skipped backup verification. Gated: the real words are never put in
+// the DOM until "Reveal", so the warning is read first.
+function backupTab() {
const shown = ui.revealShown;
const words = wallet.mnemonic.split(' ');
- 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),
@@ -446,30 +440,23 @@ function revealModal() {
return h(
'div',
- { class: 'overlay', onClick: (e) => { if (e.target.classList.contains('overlay')) close(); } },
- h(
- '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 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,
- 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')
- )
- )
+ { class: '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 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,
+ shown
+ ? h('div', { class: 'row gap6 wrap' },
+ copyBtn(wallet.mnemonic, 'Copy phrase'),
+ wallet.passphrase ? copyBtn(wallet.passphrase, 'Copy passphrase') : null,
+ h('button', { class: 'grow', onClick: () => { ui.revealShown = false; render(); } }, 'Hide')
+ )
+ : h('button', { class: 'btn-primary btn-block', onClick: () => { ui.revealShown = true; render(); } }, 'Reveal recovery phrase')
);
}
@@ -498,8 +485,7 @@ function walletScreen() {
h('div', { class: 'mt16' }, balanceCard()),
ui.offlineFallback && wallet.offline ? offlineBanner() : null,
tabsBar(),
- tabContent(),
- ui.reveal ? revealModal() : null
+ tabContent()
);
}
@@ -538,6 +524,7 @@ function tabsBar() {
['send', 'Send'],
['history', 'History'],
['coins', 'Coins'],
+ ['backup', 'Backup'],
];
return h(
'div',
@@ -545,6 +532,7 @@ function tabsBar() {
tabs.map(([id, label]) =>
tabBtn(label, ui.tab === id, () => {
ui.tab = id;
+ ui.revealShown = false; // re-mask the recovery phrase whenever tabs change
render();
})
)
@@ -557,6 +545,7 @@ function tabContent() {
case 'send': return sendTab();
case 'history': return historyTab();
case 'coins': return coinsTab();
+ case 'backup': return backupTab();
}
}
diff --git a/src/style.css b/src/style.css
index 69c26b2..9313899 100644
--- a/src/style.css
+++ b/src/style.css
@@ -206,11 +206,6 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si
transition: opacity .2s, transform .2s;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
-.overlay {
- position: fixed; inset: 0; z-index: 40; padding: 16px;
- 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; }