aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-15 21:32:44 -0700
committerAdam Soltys <[email protected]>2026-09-15 21:32:44 -0700
commit14aff677c6e091c5dd9e5f58ae7e62c855de03ae (patch)
tree1f6d9b1438961c023c03fe476ab510e2aa8d0864 /src
parent0f6fd7f06d27fa9860bd04bde13fb4ae3794fdf8 (diff)
downloadcoinosv3-14aff677c6e091c5dd9e5f58ae7e62c855de03ae.tar.gz
coinosv3-14aff677c6e091c5dd9e5f58ae7e62c855de03ae.zip
Seeing the recovery phrase once dismisses the reminder on every device.
The "still unwritten" line keyed off a flag held only in session storage and the vault, so a silent sign-in that rebuilt the account (every app open on a phone) started it over as unseen. The flag now rides the wallet's snapshot — the local cache and the synced copy, merged commutatively so it only ever turns on — and the account directory, so a rebuilt account finds it before any sync arrives. Seeing the phrase marks every account on the same seed and saves the cache at once, so it pushes to the other devices right away. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
Diffstat (limited to 'src')
-rw-r--r--src/app.js49
1 files changed, 44 insertions, 5 deletions
diff --git a/src/app.js b/src/app.js
index a35e9ee..9f17878 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1381,6 +1381,14 @@ async function activateAccount(acc, opts = {}) {
// watch-only entry (see the durable account directory) — you keep seeing your
// balance/history and re-enter the seed to spend again.
if (acc.type !== 'watch' && !acc.provisional) acc.xpub = wallet.accountXpub();
+ // an account rebuilt by a silent sign-in starts with the phrase "unseen";
+ // the directory remembers if it was seen on this device before
+ if (acc.xpub && !acc.seedSeen) {
+ try {
+ const dir = JSON.parse(localStorage.getItem(WATCH_KEY) || '[]');
+ if (dir.some((e) => e.xpub === acc.xpub && e.seedSeen)) acc.seedSeen = true;
+ } catch {}
+ }
persistAccounts();
autoSave(acc);
// A name fetched during sign-in seeds the names state BEFORE features init
@@ -1642,7 +1650,7 @@ function saveDirectory() {
|| (wallet.nostrPubkey && wallet.nostrPubkey()) || a.nostrPk;
} catch {}
}
- return { id: a.id, label: a.label, xpub: a.xpub, autoLock: a.autoLock || 0, network: a.network, nostrPk: a.nostrPk };
+ return { id: a.id, label: a.label, xpub: a.xpub, autoLock: a.autoLock || 0, network: a.network, nostrPk: a.nostrPk, seedSeen: !!a.seedSeen };
});
localStorage.setItem(WATCH_KEY, JSON.stringify(dir));
} catch {}
@@ -3000,7 +3008,7 @@ function recoveryCard(a) {
h('button', { class: 'btn-primary grow', onClick: () => {
ui.revealShown = 'words';
// seeing it here counts: no need to be asked again elsewhere
- if (!a.seedSeen) { a.seedSeen = true; persistAccounts(); if (a.persisted) writeVault(); }
+ markSeedSeen(a);
render();
} }, t('revealWords')),
// the same size as its neighbour: two halves of one row, not a
@@ -3327,9 +3335,7 @@ function onboardScreen() {
if (!acc || !acc.mnemonic) { ui.onb = null; try { localStorage.removeItem(ONB_STEP_KEY); } catch {} return null; }
const words = acc.mnemonic.split(' ');
const done = () => {
- acc.seedSeen = true;
- persistAccounts();
- if (acc.persisted) writeVault();
+ markSeedSeen(acc);
ui.onb = null;
try { localStorage.removeItem(ONB_STEP_KEY); } catch {}
ui.account = 'savings';
@@ -3617,6 +3623,39 @@ function walletScreen() {
);
}
+// The recovery phrase has been on screen: record it once, everywhere it
+// counts. Every account on the same seed is covered (one phrase backs them
+// all), the session and vault copies are written, and the wallet cache is
+// saved so the flag rides the synced snapshot to every other device — a
+// phrase read on the laptop shouldn't keep nagging on the phone. The
+// directory entry carries it too (saveDirectory), so a silent sign-in that
+// rebuilds the account on this device finds it before any sync arrives.
+function markSeedSeen(a) {
+ if (!a || a.seedSeen) return;
+ const same = (x) => x.mnemonic && x.mnemonic === a.mnemonic && (x.passphrase || '') === (a.passphrase || '');
+ for (const x of accounts) if (x === a || same(x)) x.seedSeen = true;
+ persistAccounts();
+ if (a.persisted) writeVault();
+ try { wallet.saveCache(); } catch {}
+}
+// The flag's ride on the wallet snapshot (local cache and the synced copy).
+// Commutative — it only ever turns on — so older snapshots merge too.
+wallet.registerCacheExtension({
+ mergeAlways: true,
+ save: () => ((activeAccount() || {}).seedSeen ? { seedSeen: true } : {}),
+ load: (d) => {
+ if (!d || !d.seedSeen) return;
+ const a = activeAccount();
+ if (!a || !a.mnemonic || a.seedSeen) return;
+ const same = (x) => x.mnemonic && x.mnemonic === a.mnemonic && (x.passphrase || '') === (a.passphrase || '');
+ for (const x of accounts) if (x === a || same(x)) x.seedSeen = true;
+ persistAccounts();
+ if (a.persisted) writeVault();
+ // no render here: a synced merge emits from the wallet, and a local
+ // cache load is mid-activation, which paints when it's done
+ },
+});
+
// A wallet whose recovery phrase has never been on screen keeps one quiet
// line about it. "Later" is a real answer, so this is a row and not a modal —
// but it doesn't dismiss, because the risk doesn't go away on its own. It