aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-17 23:59:14 -0700
committerAdam Soltys <[email protected]>2026-08-17 23:59:14 -0700
commit828d3b7fb6644a56938820693e8253148113fb77 (patch)
tree8d742f80e3e5c13e1c0e79589e97e42243230473 /src
parent9484c2b29a621a4413b91a88391d4f299d3323a2 (diff)
downloadcoinosv3-828d3b7fb6644a56938820693e8253148113fb77.tar.gz
coinosv3-828d3b7fb6644a56938820693e8253148113fb77.zip
An empty Spending stops being a dead end: the zap button becomes the door to boarding, prefilled with what the payment needs and open to more
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js18
-rw-r--r--src/features/zaps.js24
-rw-r--r--src/i18n.js1
3 files changed, 33 insertions, 10 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index dd71062..223fe3e 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -2133,6 +2133,24 @@ export function arkFeature(ctx) {
canLnPay() { return arkAvailable() && !wallet.watchOnly && !!(ark && ark.info); },
lnSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; },
settingsCards() { return [autoWithdrawCard()]; },
+ // 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.
+ arkOfferBoard(needSat) {
+ if (!arkAvailable() || wallet.watchOnly) return false;
+ const b = arkBalance();
+ const have = b ? b.spendableSat : 0;
+ const short = Math.max(0, Math.ceil(needSat * 1.01) + 2 - have);
+ let gross = short;
+ try { for (let i = 0; i < 4; i++) gross = short + boardFee(gross, ark.info.boardFees); } catch {}
+ const minBoard = (ark && ark.info && ark.info.minBoardAmountSat) || 0;
+ ctx.goHome();
+ ui.arkMoveOpen = true;
+ ui.arkMoveDir = 'toSpending';
+ ui.arkBoardAmt = String(Math.max(gross, minBoard));
+ render();
+ return true;
+ },
startLnPay(invoice, meta) { return startArkLnPay(invoice, meta); },
// ---- headless seam for the NWC wallet service ----
arkPayInvoice(invoice, opts) { return payInvoiceHeadless(invoice, opts); },
diff --git a/src/features/zaps.js b/src/features/zaps.js
index 0c76631..ee4c6a0 100644
--- a/src/features/zaps.js
+++ b/src/features/zaps.js
@@ -158,22 +158,24 @@ export function zapsFeature(ctx) {
const max = Math.min(Math.floor(p.maxSendable / 1000), ours != null ? ours : Infinity);
const broke = ours != null && (ours <= 0 || max < min);
const commentOk = p.allowsNostr || p.commentAllowed > 0;
- if (broke) {
- return h('div', { class: 'card col', style: 'gap:12px' },
- h('h3', {}, '⚡ ' + t('zapTitle')),
- (z.target && z.target.pk && hook('profileChip', z.target.pk, 'lg')) || h('div', { class: 'small muted', style: 'word-break:break-all' }, z.name || z.address || ''),
- h('div', { class: 'notice info' }, t('zapNoBalance')),
- h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back')));
- }
+ // An amount Spending can't cover isn't a dead end when Savings can: the
+ // primary button turns into the door to the board panel, prefilled with
+ // what this payment needs (extra is one edit away).
+ const typedSats = parseAmount(z.amount, getUnit()) || 0;
+ const theirMaxSat = Math.floor(p.maxSendable / 1000);
+ const boardable = !!hook('arkReady') && ours != null;
+ const needsBoard = boardable && (broke || (typedSats > 0 && typedSats > ours && typedSats <= theirMaxSat));
return h('div', { class: 'card col', style: 'gap:12px' },
h('h3', {}, '⚡ ' + t('zapTitle')),
(z.target && z.target.pk && hook('profileChip', z.target.pk, 'lg')) || h('div', { class: 'small muted', style: 'word-break:break-all' }, z.name || z.address || ''),
h('div', { class: 'col gap6' },
h('div', { class: 'input-group' },
h('input', { type: 'number', min: '0', inputmode: 'decimal', placeholder: t('lnPayAmount'), value: z.amount,
- onInput: (e) => { z.amount = e.target.value; } }),
+ onInput: (e) => { z.amount = e.target.value; render(); } }),
h('div', { style: 'display:flex;align-items:center' }, unitTag())),
- h('div', { class: 'small faint' }, `Min ${min.toLocaleString()} · max ${max.toLocaleString()} sats`),
+ broke
+ ? h('div', { class: 'notice info' }, t('zapNoBalance'))
+ : h('div', { class: 'small faint' }, `Min ${min.toLocaleString()} · max ${max.toLocaleString()} sats`),
commentOk
? h('input', { type: 'text', class: 'mono-input', placeholder: t('arkZapCommentPh'), value: z.comment,
maxlength: p.allowsNostr ? '280' : String(p.commentAllowed || 280),
@@ -183,7 +185,9 @@ export function zapsFeature(ctx) {
z.error ? h('div', { class: 'notice err' }, z.error) : null,
h('div', { class: 'row gap6' },
h('button', { class: 'btn-ghost', onClick: back }, t('back')),
- h('button', { class: 'btn-primary grow', onClick: confirm }, t('arkZapBtn'))));
+ needsBoard
+ ? h('button', { class: 'btn-primary grow', onClick: () => hook('arkOfferBoard', typedSats || min) }, t('zapBoardBtn'))
+ : h('button', { class: 'btn-primary grow', onClick: confirm, disabled: broke }, t('arkZapBtn'))));
}
return {
diff --git a/src/i18n.js b/src/i18n.js
index 570cdfc..7be09ed 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -195,6 +195,7 @@ const STR = {
msgNpubPlaceholder: 'npub…',
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',
msgPushOffer: 'Get notified when you receive payments and messages, even with coinos closed.',
msgPushEnable: 'Enable notifications',
msgPushOn: 'Notifications on',