From 3a0e45efce65c368dcb38e8f92a250379fc0e39a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 18 Aug 2026 00:02:52 -0700 Subject: Money finds its way to Spending: the setup wizard ends with a top-up step when Savings can fund one, and a low Spending face wears a one-tap Top up from Savings button Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz --- src/app.js | 28 +++++++++++++++++++++++++--- src/features/ark.js | 6 ++++++ src/i18n.js | 3 +++ 3 files changed, 34 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 8f5c622..b7892ea 100644 --- a/src/app.js +++ b/src/app.js @@ -2699,15 +2699,21 @@ function onboardScreen() { if (addr && addr !== o.enterAddr && !/^npub1/.test(addr)) { // The avatar ask is for brand-new profiles. An identity that already // wears a face (a nostr login with a published picture) skips straight - // to done — the punk picker restyling a real profile helps nobody. + // past the punk picker — restyling a real profile helps nobody. const me = (featureHook('nostrLoginIdentity') || {}).pubkey || (wallet.nostrPubkey && wallet.nostrPubkey()); const prof = me ? featureHook('cachedProfile', me) : null; - o.step = prof && prof.picture ? 'success' : 'avatar'; + o.step = prof && prof.picture ? nextAfterAvatar() : 'avatar'; } } try { localStorage.setItem(ONB_STEP_KEY, o.step); } catch {} const page = (kids) => h('div', { class: 'col onb', style: 'gap:20px' }, ...kids); const title = (txt) => h('h2', { class: 'onb-title' }, txt); + // After the identity niceties: offer a first top-up when Savings can fund + // one and Spending is empty-handed — a spending account with nothing in it + // is a demo, not a wallet. + const nextAfterAvatar = () => + (featureHook('arkReady') && wallet.spendable > 1000 && (featureHook('spendingSat') || 0) < 1000) + ? 'board' : 'success'; if (o.step === 'legacy') { // The migrate link carries this wallet's payment address, which the @@ -2852,7 +2858,7 @@ function onboardScreen() { try { await featureHook('publishProfile', fields, { fillOnly: o.avatar ? ['name'] : ['name', 'picture'] }); } catch (e) { toast(t('onbProfileSkipped')); console.warn('onboarding: profile publish failed —', e.message); } } - o.step = 'success'; + o.step = nextAfterAvatar(); } catch (e) { ui.onbError = e.message; } ui.onbBusy = false; render(); } }, ui.onbBusy ? h('span', { class: 'spinner sm' }) : (o.avatar ? t('onbContinue') : t('onbSkipAvatar'))), @@ -2864,6 +2870,16 @@ function onboardScreen() { } }, t('back')), ]); } + if (o.step === 'board') { + return page([ + title(t('onbBoardTitle')), + h('p', { class: 'muted', style: 'margin:0' }, t('onbBoardBody')), + featureHook('arkBoardForm') + || h('div', { class: 'row gap6', style: 'align-items:center;justify-content:center' }, + h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('arkConnecting'))), + h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => { o.step = 'success'; render(); } }, t('onbContinue')), + ]); + } // success return page([ h('div', { class: 'onb-confetti' }, ...Array.from({ length: 18 }, () => h('i'))), @@ -3162,6 +3178,12 @@ function balanceCard() { // of the niceties can't take it away — then walks the username + avatar // steps, since a spending account is best enjoyed with a payment address. // (Explicit latches carry a timestamp; self-heal only eats legacy booleans.) + // A Spending face running on fumes while Savings sits comfortable gets a + // one-tap top-up into the board panel. + hasSpending && isSpending && !ui.arkMoveOpen && spending < 1000 && wallet.spendable > 1000 + && featureHook('arkReady') && !wallet.watchOnly + ? h('button', { class: 'btn-sm', style: 'margin-top:10px', onClick: () => { ui.arkMoveDir = 'toSpending'; ui.arkMoveOpen = true; render(); } }, t('topUpFromSavings')) + : null, !hasSpending && !kindLocked && featureHook('arkReady') && !wallet.watchOnly && acc0 && acc0.type === 'full' ? h('button', { class: 'btn-sm', style: 'margin-top:10px', onClick: () => { acc0.spendingSetup = Date.now(); diff --git a/src/features/ark.js b/src/features/ark.js index 223fe3e..cdac6c4 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -2133,6 +2133,12 @@ export function arkFeature(ctx) { canLnPay() { return arkAvailable() && !wallet.watchOnly && !!(ark && ark.info); }, lnSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; }, settingsCards() { return [autoWithdrawCard()]; }, + // The onboarding wizard's top-up step borrows the board form wholesale. + arkBoardForm() { + if (!arkAvailable() || wallet.watchOnly) return null; + if (!ark) { connectArk().catch(() => {}); return null; } + return boardForm(); + }, // A payment wants more Spending than exists: open the board panel with a // prefill that nets out to the need (gross of the board fee, with a bit // of routing headroom) — editable, so boarding extra is one keystroke. diff --git a/src/i18n.js b/src/i18n.js index 7be09ed..56da989 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -196,6 +196,9 @@ const STR = { msgSearchPlaceholder: 'Name or npub…', zapNoBalance: "There's not enough in Spending to send a zap — move some money into Spending first.", zapBoardBtn: 'Move sats into Spending', + onbBoardTitle: 'Top up Spending', + onbBoardBody: 'Move some sats from Savings into Spending — payments there are instant, with no mining fees.', + topUpFromSavings: 'Top up from Savings', msgPushOffer: 'Get notified when you receive payments and messages, even with coinos closed.', msgPushEnable: 'Enable notifications', msgPushOn: 'Notifications on', -- cgit v1.2.3