aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/ark.js9
-rw-r--r--src/features/zaps.js26
-rw-r--r--src/i18n.js1
-rw-r--r--src/recipient-search.js9
4 files changed, 34 insertions, 11 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 7407bb7..5209de8 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -982,7 +982,7 @@ export function arkFeature(ctx) {
if (!z) return null;
return h('div', { class: 'card col', style: 'gap:12px' },
h('h3', {}, '⚡ ' + t('arkZapTitle')),
- h('div', { class: 'small muted', style: 'word-break:break-all' }, z.npub),
+ ctx.hook('profileChip', z.pk) || h('div', { class: 'small muted', style: 'word-break:break-all' }, z.npub),
z.status === 'lookup' ? h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('arkZapLookup'))) : null,
z.status === 'noark' ? h('div', { class: 'notice err' }, t('arkZapNoArk')) : null,
// No Ark address, but they may still take a Lightning zap — hand off to
@@ -991,7 +991,9 @@ export function arkFeature(ctx) {
? h('button', { class: 'btn-primary btn-block', onClick: () => { ctx.hook('lnZapNpub', z.pk, z.npub); } }, '⚡ ' + t('lnZapFallback'))
: null,
z.status === 'wrongnet' ? h('div', { class: 'notice err' }, t('arkGiftWrongNet', { net: z.net })) : null,
- z.status === 'ready'
+ z.status === 'ready' && (arkBalance()?.spendableSat || 0) < 330
+ ? h('div', { class: 'notice info' }, t('zapNoBalance'))
+ : z.status === 'ready'
? h('div', { class: 'col gap6' },
h('div', { class: 'input-group' },
h('input', { type: 'number', min: '0', inputmode: 'decimal', placeholder: t('lnPayAmount'), value: z.amount,
@@ -1004,7 +1006,7 @@ export function arkFeature(ctx) {
ui.sendError ? h('div', { class: 'notice err' }, ui.sendError) : null,
h('div', { class: 'row gap6' },
h('button', { class: 'btn-ghost', onClick: () => { ui.arkZap = null; ui.sendError = ''; ui.send = blankSend(); render(); } }, t('back')),
- z.status === 'ready'
+ z.status === 'ready' && (arkBalance()?.spendableSat || 0) >= 330
? (ui.busy
? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner' }))
: h('button', { class: 'btn-primary grow', onClick: doArkZap }, t('arkZapBtn')))
@@ -1604,6 +1606,7 @@ export function arkFeature(ctx) {
// The zaps feature's generic Lightning seams (once served by the retired
// swaps feature): the ASP pays invoices natively over ark.
canLnPay() { return arkAvailable() && !wallet.watchOnly && !!(ark && ark.info); },
+ lnSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; },
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 9aabf54..2b93a30 100644
--- a/src/features/zaps.js
+++ b/src/features/zaps.js
@@ -58,8 +58,9 @@ export function zapsFeature(ctx) {
const p = z.params;
if (!sats || sats <= 0) { z.error = t('enterValidAmtForN', { n: 1 }); return render(); }
const msat = sats * 1000;
- if (msat < p.minSendable || msat > p.maxSendable) {
- z.error = t('lnZapRange', { min: Math.ceil(p.minSendable / 1000).toLocaleString(), max: Math.floor(p.maxSendable / 1000).toLocaleString() });
+ const capSat = Math.min(Math.floor(p.maxSendable / 1000), hook('lnSpendableSat') ?? Infinity);
+ if (msat < p.minSendable || sats > capSat) {
+ z.error = t('lnZapRange', { min: Math.ceil(p.minSendable / 1000).toLocaleString(), max: capSat.toLocaleString() });
return render();
}
z.error = ''; z.status = 'invoicing'; render();
@@ -97,17 +98,30 @@ export function zapsFeature(ctx) {
if (z.status === 'error') {
return h('div', { class: 'card col', style: 'gap:12px' },
h('h3', {}, '⚡ ' + t('lnZapTitle')),
- h('div', { class: 'small muted', style: 'word-break:break-all' }, z.name || ''),
+ (z.target && z.target.pk && hook('profileChip', z.target.pk)) || h('div', { class: 'small muted', style: 'word-break:break-all' }, z.name || ''),
h('div', { class: 'notice err' }, z.error || t('lnZapFailed')),
h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back')));
}
- // ready: amount + optional comment
+ // ready: amount + optional comment. Their LNURL ceiling is meaningless
+ // past our own Spending balance — cap to the lower of the two, and when
+ // Spending can't even cover their minimum, say so instead of rendering
+ // an impossible range.
const p = z.params;
- const min = Math.ceil(p.minSendable / 1000), max = Math.floor(p.maxSendable / 1000);
+ const ours = hook('lnSpendableSat');
+ const min = Math.ceil(p.minSendable / 1000);
+ 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('lnZapTitle')),
+ (z.target && z.target.pk && hook('profileChip', z.target.pk)) || 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')));
+ }
return h('div', { class: 'card col', style: 'gap:12px' },
h('h3', {}, '⚡ ' + t('lnZapTitle')),
- h('div', { class: 'small muted', style: 'word-break:break-all' }, z.name || z.address || ''),
+ (z.target && z.target.pk && hook('profileChip', z.target.pk)) || 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,
diff --git a/src/i18n.js b/src/i18n.js
index d1587a0..9b34361 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -169,6 +169,7 @@ const STR = {
msgOpen: 'Open',
msgNpubPlaceholder: 'npub…',
msgSearchPlaceholder: 'Name or npub…',
+ zapNoBalance: "There's not enough in Spending to send a zap — move some money into Spending first.",
msgPushOffer: 'Get notified when you receive payments and messages, even with coinos closed.',
msgPushEnable: 'Enable notifications',
msgPushOn: 'Notifications on',
diff --git a/src/recipient-search.js b/src/recipient-search.js
index 553673e..b8a17ea 100644
--- a/src/recipient-search.js
+++ b/src/recipient-search.js
@@ -140,11 +140,16 @@ export function makeSearcher(onUpdate) {
clearTimeout(timer);
const my = ++seq;
if (!searchable(q)) { onUpdate(q, null); return; }
- const cached = queryCache.has(q.trim().toLowerCase());
+ const s = q.trim().toLowerCase();
+ // Get something on screen fast, then relax: the first short query fires
+ // almost immediately (that's the wait the user actually feels), while
+ // longer ones — mid-word keystrokes — wait long enough to skip the
+ // characters still being typed. Cached prefixes are instant.
+ const wait = queryCache.has(s) ? 0 : s.length <= 3 ? 120 : 500;
timer = setTimeout(async () => {
const rows = await searchRecipients(q).catch(() => []);
if (my === seq) onUpdate(q, rows);
- }, cached ? 0 : 300);
+ }, wait);
},
clear() { seq++; clearTimeout(timer); onUpdate('', null); },
};