aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-06 19:00:32 -0700
committerAdam Soltys <[email protected]>2026-09-06 19:00:32 -0700
commit966ef24380bc25fb0f0a064358c802347cb965a3 (patch)
tree9954036d7368d0d499b31309fc7d72160ba84208 /src
parent2e492ec499264ba84ec8cad30f788eff28556269 (diff)
downloadcoinosv3-966ef24380bc25fb0f0a064358c802347cb965a3.tar.gz
coinosv3-966ef24380bc25fb0f0a064358c802347cb965a3.zip
LNURL auto-pay: budget presets + skip the review-screen flash on an armed pay.
(1) The auto-pay opt-in on a fixed-amount merchant pay was a single checkbox that stored "up to the amount you just paid". Replaced with preset budget buttons — 1000 / 5000 / 10000 / 100000 sats — so you set the ceiling deliberately; the chosen amount is what's stored on a successful pay. (2) An NFC tap into an already-armed auto-pay briefly flashed the review/confirm card (the "Getting a quote…" spinner) before advancing to pay. startArkLnPay now decides up front — from the invoice amount and the stored budget — whether it will auto-pay, and the view renders a centered paying screen straight away (matching the paid screen's layout so it morphs cleanly into it), never showing the review. You just see paying → paid. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js42
-rw-r--r--src/i18n.js1
2 files changed, 31 insertions, 12 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 46ec701..fa08289 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -851,7 +851,12 @@ export function arkFeature(ctx) {
// pending funds count for the take-over decision: an in-flight payment or
// revocation frees them in seconds
if (sum('spendable') < (dec.amountSat || 1) && sum('pending') < (dec.amountSat || 1)) return false;
- ui.arkLnPay = { invoice, meta: meta || null, amountSat: dec.amountSat, amount: '', feeSat: null, status: 'quote' };
+ // Will this pay itself (an armed budget that covers it)? Known before the
+ // quote — from the invoice amount and the stored budget — so the view can
+ // go straight to a paying screen and never flash the review card first.
+ const armedBudget = meta && meta.autopay && meta.budgetKey && ctx.lnBudget ? ctx.lnBudget(meta.budgetKey) : 0;
+ const autopaying = !!(meta && meta.autopay && dec.amountSat != null && armedBudget > 0 && dec.amountSat <= armedBudget);
+ ui.arkLnPay = { invoice, meta: meta || null, amountSat: dec.amountSat, amount: '', feeSat: null, status: 'quote', autopaying };
ui.sendError = '';
render();
// An auto-pay recipient (a budget the user set) pays as soon as the quote
@@ -957,10 +962,10 @@ export function arkFeature(ctx) {
if (a.step === 'done') {
ui.arkLnPaid = { amountSat: a.amountSat, meta: p.meta };
if (p.meta && p.meta.pk) noteZap('inv:' + p.invoice, p.meta.pk);
- // the user opted this recipient into auto-pay: remember the budget
- // so the next fixed charge from them clears without a tap
+ // the user opted this recipient into auto-pay: remember the chosen
+ // budget so future fixed charges from them clear without a tap
if (p.setBudget && p.meta && p.meta.budgetKey && ctx.setLnBudget)
- ctx.setLnBudget(p.meta.budgetKey, a.amountSat);
+ ctx.setLnBudget(p.meta.budgetKey, p.setBudget);
ui.arkLnPay = null;
} else if (ui.arkLnPay) {
if (tries < 2 && lnRetryable(a)) {
@@ -1029,6 +1034,16 @@ export function arkFeature(ctx) {
const p = ui.arkLnPay;
if (!p) return null;
const zap = p.meta;
+ // Auto-pay: show only a paying screen (matching the paid screen's layout, so
+ // it morphs into it) instead of flashing the review card before it pays.
+ if (p.autopaying) {
+ return h('div', { class: 'card col', style: 'align-items:center;text-align:center;gap:14px;padding:48px 20px' },
+ h('div', { class: 'check-badge' }, '⚡'),
+ h('h2', { style: 'margin:0' }, t('arkLnPaying')),
+ zap && zap.name ? h('div', { class: 'small muted' }, zap.name) : null,
+ h('div', { class: 'amount-neg', style: 'font-size:18px' }, '-' + fmtAmount(p.amountSat) + u),
+ h('span', { class: 'spinner' }));
+ }
const row = (label, sats, bold) => h('div', { class: 'row between' + (bold ? '' : ''), style: bold ? 'font-weight:600' : '' },
h('span', { class: bold ? '' : 'small muted' }, label), h('span', {}, fmtAmount(sats) + u));
const total = (p.amountSat || 0) + (p.feeSat || 0);
@@ -1057,17 +1072,20 @@ export function arkFeature(ctx) {
? [h('div', { style: 'border-top:1px solid var(--line, #ddd);margin:2px 0' }), row(t('lnPayTotal'), total, true)]
: null),
ui.sendError ? h('div', { class: 'notice err' }, ui.sendError) : null,
- // Offer to skip this confirmation next time: a fixed-amount merchant
- // pay with no budget yet can be trusted up to this amount. Ticking it
- // stores the budget on a successful pay (see settle); after that the
- // same recipient auto-pays without this screen.
+ // Offer to skip this confirmation next time: pick a budget and future
+ // fixed charges from this recipient up to it auto-pay without this
+ // screen. The chosen amount is stored on a successful pay (see settle).
(zap && zap.budgetKey && zap.fixedSat && !zap.autopay
&& p.status === 'ready'
&& !(ctx.lnBudget && ctx.lnBudget(zap.budgetKey) > 0))
- ? h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer' },
- h('input', { type: 'checkbox', checked: !!p.setBudget, style: 'flex:0 0 auto;margin:0',
- onChange: (e) => { p.setBudget = e.target.checked; } }),
- h('span', { class: 'small muted' }, t('lnAutopayOffer', { name: zap.name || zap.address || '', n: fmtAmount(zap.fixedSat) + ' ' + unitLabel() })))
+ ? h('div', { class: 'col', style: 'gap:6px' },
+ h('span', { class: 'small muted' }, t('lnAutopaySetLabel', { name: zap.name || zap.address || '' })),
+ h('div', { class: 'row gap6', style: 'flex-wrap:wrap' },
+ ...[1000, 5000, 10000, 100000].map((amt) =>
+ h('button', { type: 'button',
+ class: (p.setBudget === amt ? 'btn-primary' : 'btn-ghost') + ' btn-sm',
+ onClick: () => { p.setBudget = p.setBudget === amt ? 0 : amt; render(); } },
+ fmtAmount(amt) + ' ' + unitLabel()))))
: null,
p.status === 'paying'
? h('div', { class: 'card row gap6', style: 'align-items:center' },
diff --git a/src/i18n.js b/src/i18n.js
index dbc23f7..84e4b5b 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -914,6 +914,7 @@ const STR = {
giftChainWarn: '⚠ This is a bearer link: whoever opens it can claim the gift, so share it privately. It funds a fresh on-chain wallet when claimed.',
giftRetired: 'This gift link uses an older format that’s no longer safe to claim. Ask the sender to void it and create a new one.',
lnAutopayOffer: 'Auto-pay {name} up to {n} next time',
+ lnAutopaySetLabel: 'Auto-pay {name} next time, up to:',
lnPaySentTitle: 'Lightning payment sent',
lnPaySentBody: 'The swap provider pays the invoice once the on-chain lockup confirms.',
lnZapTitle: 'Zap over Lightning',