diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 09:56:56 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 09:56:56 -0700 |
| commit | 258f815bea0bd0e44091bda8b48b83ff15e39aa6 (patch) | |
| tree | aa3605341ce34e6a8874100b73b608e3156dea85 /src | |
| parent | 6cce1f7fdbf487222e919bc2f50870d539f19b8c (diff) | |
| download | coinosv3-258f815bea0bd0e44091bda8b48b83ff15e39aa6.tar.gz coinosv3-258f815bea0bd0e44091bda8b48b83ff15e39aa6.zip | |
Zap: resolve inside a skeleton of the card that's coming
The inline-progress attempt still read as a flash — the send form was
visible behind it with the raw npub, its verification block and the
gift button, then all of that was replaced. Now both resolution phases
(ark advert, then lightning) render the SHAPE of the destination card:
same title, same recipient chip, a spinner where the amount form will
be, and a working Back. The card fills in instead of the screen
changing twice, and the two flows share one title so it never
re-titles at the handoff.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 7 | ||||
| -rw-r--r-- | src/features/ark.js | 23 | ||||
| -rw-r--r-- | src/features/zaps.js | 19 | ||||
| -rw-r--r-- | src/i18n.js | 2 |
4 files changed, 35 insertions, 16 deletions
@@ -2812,13 +2812,6 @@ function recipientRow(s, r, i) { !single && h('button', { type: 'button', class: 'btn-sm', title: t('remove'), onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕') ), check, - i === 0 ? (() => { - const note = featureHook('resolvingNote'); - return note - ? h('div', { class: 'row gap6', style: 'align-items:center;padding:2px 0' }, - h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, note)) - : null; - })() : null, i === 0 && sendSearch.rows && sendSearch.rows.length && searchable(r.address) ? h('div', { class: 'list send-suggest' }, resultRows(h, sendSearch.rows, pickRecipient)) : null, diff --git a/src/features/ark.js b/src/features/ark.js index 7fda8ed..5c5e0a8 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -929,6 +929,20 @@ export function arkFeature(ctx) { })().catch((e) => { if (live()) { z.status = 'noark'; ui.sendError = e.message; render(); } }); } + // 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' }, + h('h3', {}, '⚡ ' + t('zapTitle')), + (pk && ctx.hook('profileChip', pk, 'lg')) + || h('div', { class: 'small muted break' }, fallback || ''), + h('div', { class: 'row gap6', style: 'align-items:center;padding:6px 0' }, + h('span', { class: 'spinner sm' }), + h('span', { class: 'small muted' }, t('zapFinding'))), + h('button', { class: 'btn-ghost btn-block', onClick: () => { + ui.arkZap = null; ui.zap = null; ui.sendError = ''; ui.send = blankSend(); render(); + } }, t('back'))); + } + async function lookupArkZapTarget(pk) { const events = await wallet.nostrFetch({ kinds: [ARK_INFO_KIND], authors: [pk] }, 6000); const ev = (events || []).sort((a, b) => b.created_at - a.created_at)[0]; @@ -967,8 +981,10 @@ export function arkFeature(ctx) { } function arkZapView() { - // the npub lookup is a background step — no takeover card for it - if (ui.arkZap && ui.arkZap.status === 'lookup') return null; + // While resolving, show the SHAPE of the card that's coming — same + // title, same recipient chip, a spinner where the form will be. The + // screen fills in rather than being replaced. + if (ui.arkZap && ui.arkZap.status === 'lookup') return zapSkeleton(ui.arkZap.pk, ui.arkZap.npub); if (ui.arkZapped) { return h('div', { class: 'card col', @@ -983,7 +999,7 @@ export function arkFeature(ctx) { const z = ui.arkZap; if (!z) return null; return h('div', { class: 'card col', style: 'gap:12px' }, - h('h3', {}, '⚡ ' + t('arkZapTitle')), + h('h3', {}, '⚡ ' + t('zapTitle')), ctx.hook('profileChip', z.pk, 'lg') || 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, @@ -1636,7 +1652,6 @@ export function arkFeature(ctx) { // 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; }, - resolvingNote() { return ui.arkZap && ui.arkZap.status === 'lookup' ? t('arkZapLookup') : null; }, 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 3298eaf..18676de 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -93,7 +93,17 @@ export function zapsFeature(ctx) { // Resolution is a background step, not a screen: the send form stays put // and shows one inline line (resolvingNote below) until we can render // something the user can act on. - if (z.status === 'resolving') return null; + if (z.status === 'resolving') { + // twin of ark's skeleton: identical shape, so the handoff is invisible + 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 break' }, z.name || z.address || ''), + h('div', { class: 'row gap6', style: 'align-items:center;padding:6px 0' }, + h('span', { class: 'spinner sm' }), + h('span', { class: 'small muted' }, t('zapFinding'))), + h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back'))); + } if (z.status === 'invoicing') { return h('div', { class: 'card col', style: 'align-items:center;gap:14px;padding:32px 14px' }, h('span', { class: 'spinner' }), @@ -101,7 +111,7 @@ export function zapsFeature(ctx) { } if (z.status === 'error') { return h('div', { class: 'card col', style: 'gap:12px' }, - h('h3', {}, '⚡ ' + t('lnZapTitle')), + 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 || ''), h('div', { class: 'notice err' }, z.error || t('lnZapFailed')), h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back'))); @@ -118,13 +128,13 @@ export function zapsFeature(ctx) { const commentOk = p.allowsNostr || p.commentAllowed > 0; if (broke) { return h('div', { class: 'card col', style: 'gap:12px' }, - h('h3', {}, '⚡ ' + t('lnZapTitle')), + 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'))); } return h('div', { class: 'card col', style: 'gap:12px' }, - h('h3', {}, '⚡ ' + t('lnZapTitle')), + 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' }, @@ -186,7 +196,6 @@ export function zapsFeature(ctx) { // True when an npub can be zapped over Lightning from this build/wallet — // ark's "no Ark address" screen checks this before offering the fallback. canLnZap() { return canPay() && !!wallet.nostrProfile; }, - resolvingNote() { return ui.zap && ui.zap.status === 'resolving' ? t('lnZapResolving') : null; }, // Ark's "no Ark address published" screen offers a Lightning fallback, // which lands here with the recipient's pubkey. lnZapNpub(pk, npub) { diff --git a/src/i18n.js b/src/i18n.js index b387285..e87f097 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -463,6 +463,8 @@ const STR = { arkUniDesc: 'Your escape hatch: claim the money yourself without anyone’s help. Takes about a day and costs mining fees, but works even if the server disappears.', arkExitNoBalance: 'Nothing in Spending to move.', arkZapTitle: 'Send a zap', + zapTitle: 'Send a zap', + zapFinding: 'Finding the best way to pay them…', arkZapLookup: 'Looking them up…', arkZapNoArk: 'No way to pay this account was found — no Ark address, Lightning address, or payment record.', arkZapHint: 'Instant and free, with a zap receipt on nostr.', |
