aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-28 07:57:09 -0700
committerAdam Soltys <[email protected]>2026-08-28 07:57:09 -0700
commit274130edd7d11a0643d9b1f7469022207e5f5563 (patch)
tree46a0e288bebd4669a0bbe3d8850138d8932d90a8 /src
parent92dd80a816c3efdabfd9aaabb3461d9078b39b5e (diff)
downloadcoinosv3-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.js9
-rw-r--r--src/features/nostrlogin.js19
-rw-r--r--src/i18n.js3
-rw-r--r--src/nostr-login.js9
4 files changed, 28 insertions, 12 deletions
diff --git a/src/app.js b/src/app.js
index 9435cc3..5b5c3d5 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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