diff options
| author | Adam Soltys <[email protected]> | 2026-08-15 12:53:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-15 12:53:23 -0700 |
| commit | d45c2f2e5a3a7c7e50b701a82c15866a8dc96811 (patch) | |
| tree | e3c539e761678bd3eee3eb63cdbf33e4de0a9141 /src/features/zaps.js | |
| parent | 19ec7b5493009e36a5232bf3a7d13caf17330f83 (diff) | |
| download | coinosv3-d45c2f2e5a3a7c7e50b701a82c15866a8dc96811.tar.gz coinosv3-d45c2f2e5a3a7c7e50b701a82c15866a8dc96811.zip | |
The social layer grows up: a header magnifier finds anyone on nostr, ⚡ zaps your saved amount in one tap (asking once if you haven't picked one), and tapping a note opens its thread
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
Diffstat (limited to 'src/features/zaps.js')
| -rw-r--r-- | src/features/zaps.js | 52 |
1 files changed, 50 insertions, 2 deletions
diff --git a/src/features/zaps.js b/src/features/zaps.js index 3f1b599..0c76631 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -16,7 +16,7 @@ import { npubOf } from '../nostr.js'; import { parseZapTarget, zapTargetFromProfile, fetchPayParams, buildZapRequest, requestInvoice } from '../lnurl.js'; export function zapsFeature(ctx) { - const { h, ui, render, wallet, blankSend, parseAmount, getUnit, unitTag, hook } = ctx; + const { h, ui, render, wallet, blankSend, parseAmount, getUnit, unitTag, hook, toast, fmtAmount, unitLabel } = ctx; // Lightning payment has to be possible for a zap to make sense. const canPay = () => !wallet.watchOnly && !!hook('canLnPay'); @@ -52,6 +52,36 @@ export function zapsFeature(ctx) { render(); } + // One-tap zap at the user's default amount: resolve, invoice, pay through + // the ark seam (its quote + retries included), and report by toast — the + // whole flow without a single screen. + async function autoZap(target, sats) { + const profile = await wallet.nostrProfile(target.pk); + const lnTarget = zapTargetFromProfile(profile, target.pk); + if (!lnTarget) throw new Error(t('lnZapNoAddress')); + const t2 = { ...lnTarget, eventId: target.eventId || null }; + const p = await fetchPayParams(t2.url); + const msat = sats * 1000; + if (msat < p.minSendable || msat > p.maxSendable) { + throw new Error(t('lnZapRange', { + min: Math.ceil(p.minSendable / 1000).toLocaleString(), + max: Math.floor(p.maxSendable / 1000).toLocaleString(), + })); + } + let zapRequest = null; + if (p.allowsNostr && t2.pk && wallet.nostrSign) { + zapRequest = buildZapRequest({ + amountMsat: msat, relays: wallet.nostrRelays(), lnurlBech32: t2.lnurlBech32, + recipientPk: t2.pk, eventId: t2.eventId || null, comment: '', + signFn: (partial) => wallet.nostrSign(partial), + }); + } + const invoice = await requestInvoice(p, { amountMsat: msat, zapRequest, lnurlBech32: t2.lnurlBech32 }); + await hook('arkPayInvoice', invoice, { maxAmountSat: sats + 50 }); + toast('⚡ ' + t('zapSentShort', { n: fmtAmount(sats) + ' ' + unitLabel() })); + render(); + } + async function confirm() { const z = ui.zap; if (!z || z.status !== 'ready') return; @@ -201,11 +231,29 @@ export function zapsFeature(ctx) { // Ark's "no Ark address published" screen offers a Lightning fallback, // which lands here with the recipient's pubkey (and, when the zap was // aimed at a note, its event id for the zap request's e tag). - lnZapNpub(pk, npub, eventId) { + lnZapNpub(pk, npub, eventId, autoSat) { if (!canPay() || !wallet.nostrProfile) return false; + if (autoSat && hook('arkReady')) { + autoZap({ kind: 'npub', pk, eventId: eventId || null }, autoSat) + .catch((e) => toast('⚡ ' + e.message)); + return true; + } begin({ kind: 'npub', pk, eventId: eventId || null }, shortNpub(npub || npubOf(pk))); return true; }, sendView() { return zapView(); }, + // Settings → Payments: the one-tap zap amount. + settingsCards() { + const cur = ctx.zapDefaultSat ? ctx.zapDefaultSat() : 0; + return [h('div', { class: 'card col', style: 'gap:8px' }, + h('div', { class: 'row between', style: 'align-items:center;gap:10px' }, + h('span', { class: 'small' }, '⚡ ' + t('zapDefaultLabel')), + h('div', { class: 'input-group', style: 'width:130px' }, + h('input', { type: 'number', min: '1', value: cur || '', placeholder: '21', + style: 'text-align:right', + onInput: (e) => { const n = parseInt(e.target.value, 10); if (n > 0) ctx.setZapDefaultSat(n); } }), + h('span', { class: 'small muted', style: 'align-self:center;padding:0 6px' }, 'sats'))), + h('div', { class: 'small faint' }, t('zapDefaultHint')))]; + }, }; } |
