diff options
| author | Adam Soltys <[email protected]> | 2026-09-21 16:38:30 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-21 16:38:30 -0700 |
| commit | 05a98a9110f5e2e2476d9f9e948ee79be5effe73 (patch) | |
| tree | cd69cd52efafe93b1fd5c68f2967fbab6eb0c5c0 /src/features/ark.js | |
| parent | 9a29372359c0c998c51e5b82188202c779621fc8 (diff) | |
| download | coinosv3-05a98a9110f5e2e2476d9f9e948ee79be5effe73.tar.gz coinosv3-05a98a9110f5e2e2476d9f9e948ee79be5effe73.zip | |
Send: a Lightning payment while Savings is showing asks before it pays from Spending.
Every Lightning payment passes through the Ark flow, whichever balance is
on screen, and the review's "from your Spending balance" line was easy to
miss. With Savings selected the flow now stops at an ask first: switch to
Spending and continue, move Savings into Spending first (the existing
board panel — an on-chain step, so a fresh invoice afterwards), or cancel.
Pasted invoices, Lightning addresses and LNURL all pass through; autopay
(a trusted, budgeted code) never asks.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
Diffstat (limited to 'src/features/ark.js')
| -rw-r--r-- | src/features/ark.js | 41 |
1 files changed, 41 insertions, 0 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index 9b3173e..ed494b0 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -923,9 +923,20 @@ export function arkFeature(ctx) { // A bolt11 lands in Send (via the swaps feature's delegation hook, or // directly when swaps is absent): take over when the ark balance can // plausibly cover it, else decline so the Boltz path handles it. + // Lightning leaves Spending, whichever balance is on screen. With Savings + // showing, ask before going on — a person looking at one balance and + // charged from the other read it as the wrong wallet paying. Autopay + // (a trusted, budgeted code) never asks: nobody is looking. function startArkLnPay(invoice, meta) { const dec = maybeLnInvoice(invoice); if (!dec || !arkAvailable() || wallet.watchOnly) return false; + if (ctx.getAccount() === 'savings' && !(meta && meta.autopay) && !ui.arkLnFromSavingsOk) { + ui.arkLnFromSavings = { invoice, meta: meta || null, amountSat: dec.amountSat }; + ui.sendError = ''; + render(); + return true; + } + ui.arkLnFromSavingsOk = false; const s = arkStateNow(); // Coverage by SUM: multi-input pays gather up to 24 coins, so judging by // the single largest coin declined invoices the wallet could pay — a @@ -1651,6 +1662,35 @@ export function arkFeature(ctx) { })().catch((e) => { if (live()) { z.status = 'noark'; if (!bail(e.message)) ui.sendError = e.message; render(); } }); } + // The ask itself: switch and continue, move Savings over first, or not. + function arkLnFromSavingsView() { + const p = ui.arkLnFromSavings; + const have = arkBalance()?.spendableSat || 0; + const need = p.amountSat || 0; + const enough = !need || have >= need; + const proceed = () => { + ui.arkLnFromSavings = null; + ctx.setAccount('spending', 'right'); + ui.arkLnFromSavingsOk = true; // this one call is the answer to the ask + if (startArkLnPay(p.invoice, p.meta)) return; + ui.arkLnFromSavingsOk = false; + // Spending can't carry it: the same offer the Spending side makes + if (need && wallet.spendable > 1000 && ctx.hook('arkOfferBoard', need)) return; + ui.sendError = t('arkLnExceedsSpending', { need: fmtAmount(need) + ' ' + unitLabel(), have: fmtAmount(have) + ' ' + unitLabel() }); + render(); + }; + return h('div', { class: 'card col', style: 'gap:12px' }, + h('h3', { style: 'margin:0' }, t('arkLnFromSavingsTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, t('arkLnFromSavingsBody', { have: fmtAmount(have) + ' ' + unitLabel() })), + need ? h('div', { class: 'row between' }, h('span', { class: 'small muted' }, t('lnPayAmount')), h('span', { class: 'small' }, fmtAmount(need) + ' ' + unitLabel())) : null, + !enough ? h('div', { class: 'notice info small' }, t('arkLnFromSavingsShort')) : null, + h('button', { class: 'btn-primary btn-block', onClick: proceed }, enough ? t('arkLnUseSpending') : t('arkLnMoveFirst')), + enough && wallet.spendable > 1000 + ? h('button', { class: 'btn-block', onClick: () => { ui.arkLnFromSavings = null; ctx.hook('arkOfferBoard', need || 0); } }, t('arkLnMoveFirst')) + : null, + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.arkLnFromSavings = null; ui.sendError = ''; ui.send = blankSend(); render(); } }, t('cancel'))); + } + // shared by the ark and lightning zap flows (see zaps.js for the twin) function zapSkeleton(pk, fallback) { return h('div', { class: 'card col', style: 'gap:12px' }, @@ -3214,6 +3254,7 @@ export function arkFeature(ctx) { return false; }, sendView() { + if (ui.arkLnFromSavings) return arkLnFromSavingsView(); if (ui.arkOffboardSend) return arkOffboardSendView(); if (ui.arkLnPaid || ui.arkLnPay) return arkLnPayView(); if (ui.arkZapped || ui.arkZap) return arkZapView(); |
