diff options
| author | Adam Soltys <[email protected]> | 2026-08-28 07:57:09 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-28 07:57:09 -0700 |
| commit | 274130edd7d11a0643d9b1f7469022207e5f5563 (patch) | |
| tree | 46a0e288bebd4669a0bbe3d8850138d8932d90a8 /src | |
| parent | 92dd80a816c3efdabfd9aaabb3461d9078b39b5e (diff) | |
| download | coinosv3-274130edd7d11a0643d9b1f7469022207e5f5563.tar.gz coinosv3-274130edd7d11a0643d9b1f7469022207e5f5563.zip | |
The wallet remembers it has a spending side: the relay backup carries the hint, stamped the moment spending becomes real, so a restore paints Spending from its first frame instead of reflowing when the name lookup catches up — and the sign-in toasts are gone, since a wallet appearing announces itself
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 9 | ||||
| -rw-r--r-- | src/features/nostrlogin.js | 19 | ||||
| -rw-r--r-- | src/i18n.js | 3 | ||||
| -rw-r--r-- | src/nostr-login.js | 9 |
4 files changed, 28 insertions, 12 deletions
@@ -1111,6 +1111,10 @@ async function activateAccount(acc, opts = {}) { // current global choice once, then keep it. The global setting remains the // default for the NEXT new wallet. if (!acc.network) acc.network = getNetwork(); + // The relay backup said this wallet has a Spending side: latch it BEFORE + // the first paint, so a restored wallet doesn't open as savings-only and + // reflow when the name lookup proves Spending a beat later. + if (opts.spendingHint && !acc.spendingSetup) { acc.spendingSetup = Date.now(); } const netName = acc.network; if (netName !== getNetwork()) { setNetwork(netName); @@ -2954,7 +2958,7 @@ function onboardScreen() { const spendReady = claimedNow || (featureHook('spendingSat') || 0) > 0 || (getNetwork() === 'mainnet' && named); if (spendReady) { - if (acc && !acc.spendingSetup) { acc.spendingSetup = Date.now(); persistAccounts(); } + if (acc && !acc.spendingSetup) { acc.spendingSetup = Date.now(); persistAccounts(); featureHook('spendingEvident'); } ui.account = 'spending'; try { localStorage.setItem(ACCOUNT_KEY, 'spending'); } catch {} } @@ -3095,7 +3099,7 @@ function spendingActive() { const evident = (featureHook('spendingSat') || 0) > 0 || (!!addr && !/^npub1/.test(addr)); // Latch demonstrated use onto the record, so the Wallets list keeps showing // this seed's Spending side even while it's locked or not the active one. - if (evident && acc && !acc.spendingSetup) { acc.spendingSetup = Date.now(); persistAccounts(); } + if (evident && acc && !acc.spendingSetup) { acc.spendingSetup = Date.now(); persistAccounts(); featureHook('spendingEvident'); } return evident; } // ---- the flat wallet model ------------------------------------------------- @@ -3239,6 +3243,7 @@ function balanceCard() { ? h('button', { class: 'btn-sm', style: 'margin-top:10px', onClick: () => { acc0.spendingSetup = Date.now(); persistAccounts(); + featureHook('spendingEvident'); setAccountSel('spending', 'right'); ui.onbError = ''; ui.onb = { step: 'spend' }; diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index dbf2ece..6c0889b 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -36,6 +36,7 @@ export function nostrLoginFeature(ctx) { // the wallet hasn't been persisted yet, so init() must not sever it let attaching = false; let resuming = null; + let _spendingHinted = false; // relay backup already carries the spending hint let resumeFailedAt = 0; // A remote signer's connection can die quietly — the app is left holding a @@ -102,6 +103,7 @@ export function nostrLoginFeature(ctx) { // so an extension login later finds this wallet instead of making a new // one. Costs nothing — anyone with the key could derive it anyway. if (res.publish) await publishWalletBackup(signer, { mnemonic: res.mnemonic }).catch(() => {}); + const spendingHint = !!res.spending; attaching = true; live = selfHealing(signer); // hand the wizard over BEFORE the screen flips: walletScreen routes @@ -109,12 +111,12 @@ export function nostrLoginFeature(ctx) { // re-animated the welcome screen for a beat before the wallet appeared ctx.onbNostrLogin(res.mode === 'restored'); ui.navAnimSkip = true; // the handover swaps instantly, no fade - await ctx.openMnemonic(res.mnemonic, res.passphrase || '', { nostrPubkey: signer.pubkey }); + await ctx.openMnemonic(res.mnemonic, res.passphrase || '', { nostrPubkey: signer.pubkey, spendingHint }); save({ ...load(), pubkey: signer.pubkey, linked: Date.now(), ...sessionOf(signer) }); attaching = false; // claim the real npub as the payment address while this signer is live ctx.hook('namesAdoptIdentity', signer, npubOf(signer.pubkey))?.catch?.(() => {}); - toast(res.mode === 'derived' ? t('nlOpenedDerived') : t('nlOpenedRestored')); + // no success toast: the wallet appearing IS the message busy(false); } catch (e) { fail(e); } } @@ -133,11 +135,10 @@ export function nostrLoginFeature(ctx) { live = selfHealing(st.signer); ctx.onbNostrLogin(!!existing); // before the screen flips — see loginWith ui.navAnimSkip = true; // the handover swaps instantly, no fade - await ctx.openMnemonic(mnemonic, (existing && existing.passphrase) || '', { nostrPubkey: st.signer.pubkey }); + await ctx.openMnemonic(mnemonic, (existing && existing.passphrase) || '', { nostrPubkey: st.signer.pubkey, spendingHint: !!(existing && existing.spending) }); save({ ...load(), pubkey: st.signer.pubkey, linked: Date.now(), ...sessionOf(st.signer) }); attaching = false; ctx.hook('namesAdoptIdentity', st.signer, npubOf(st.signer.pubkey))?.catch?.(() => {}); - toast(existing ? t('nlOpenedRestored') : t('nlCreated')); busy(false); } catch (e) { fail(e); } } @@ -585,6 +586,16 @@ export function nostrLoginFeature(ctx) { unlockExtra() { return unlockExtra(); }, // The welcome screen's sign-in block: Google and passkey act right there; // the Nostr button steps into the wizard's signer list. + // Spending just became real (name claimed / balance appeared): stamp the + // hint into the relay backup while a signer is live, so the NEXT restore + // paints Spending from its first frame. Once per session is plenty. + spendingEvident() { + if (_spendingHinted || !live || !wallet.mnemonic) return null; + _spendingHinted = true; + publishWalletBackup(live, { mnemonic: wallet.mnemonic, passphrase: wallet.passphrase || '', spending: true }) + .catch(() => { _spendingHinted = false; }); + return true; + }, frontDoorSignin() { return h('div', { class: 'col', style: 'gap:8px' }, ...externalButtons(loginWith, true), diff --git a/src/i18n.js b/src/i18n.js index c08facd..2aa41b6 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -637,13 +637,10 @@ const STR = { nlHint: 'Works with Amber, Amethyst and other remote signers (paste their bunker:// address), or a browser extension. A pasted key is used once and never stored.', nlApprove: 'Approve the request in your signer →', nlUnrecognized: 'That doesn’t look like a private key or a bunker:// address.', - nlOpenedDerived: 'Wallet opened from your Nostr key', - nlOpenedRestored: 'Your wallet was restored', nlNewTitle: 'Create a wallet for this account', nlNewDesc: 'This Nostr account has no wallet yet. We’ll make one and save it, encrypted to your account, so you can open it anywhere you use this account.', nlBackupWarning: 'Your wallet will be recoverable by anyone who controls this Nostr account — treat that account like a key to your money.', nlCreateBtn: 'Create my wallet', - nlCreated: 'Wallet created and linked', nlLinkTitle: 'Nostr account', nlLinkDesc: 'Log in with your own Nostr account instead — chat and your payment address will use its identity, and logging in with it on another device opens this wallet.', nlBuiltinDesc: 'This wallet has a built-in Nostr account, derived from its seed. Chat and your payment address already use it — you don’t need to link anything.', diff --git a/src/nostr-login.js b/src/nostr-login.js index b032c73..514c36b 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -233,8 +233,11 @@ export async function fetchWalletBackup(signer, relays = BACKUP_RELAYS) { } // Publish (or replace) the encrypted wallet association for this account. -export async function publishWalletBackup(signer, { mnemonic, passphrase }, relays = BACKUP_RELAYS) { - const content = await signer.encryptSelf(JSON.stringify({ mnemonic, passphrase: passphrase || '' })); +// `spending` rides along as a hint: a restored wallet paints its Spending +// side from the first frame instead of reflowing when the name lookup proves +// it seconds later. +export async function publishWalletBackup(signer, { mnemonic, passphrase, spending }, relays = BACKUP_RELAYS) { + const content = await signer.encryptSelf(JSON.stringify({ mnemonic, passphrase: passphrase || '', ...(spending ? { spending: true } : {}) })); const evt = await signer.signEvent({ kind: BACKUP_KIND, created_at: Math.floor(Date.now() / 1000), tags: [['d', SEED_DTAG]], content, @@ -256,7 +259,7 @@ export async function publishWalletBackup(signer, { mnemonic, passphrase }, rela // deriving (key in hand) or generating (signer only). export async function walletForSigner(signer) { const found = await fetchWalletBackup(signer); - if (found) return { mnemonic: found.mnemonic, passphrase: found.passphrase, mode: 'restored' }; + if (found) return { mnemonic: found.mnemonic, passphrase: found.passphrase, spending: !!found.spending, mode: 'restored' }; if (signer.secret) { // Publishing the derived seed costs nothing: the derivation is public, // so anyone holding this key could compute it anyway. It buys the thing |
