aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 09:56:56 -0700
committerAdam Soltys <[email protected]>2026-08-03 09:56:56 -0700
commit258f815bea0bd0e44091bda8b48b83ff15e39aa6 (patch)
treeaa3605341ce34e6a8874100b73b608e3156dea85 /src
parent6cce1f7fdbf487222e919bc2f50870d539f19b8c (diff)
downloadcoinosv3-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.js7
-rw-r--r--src/features/ark.js23
-rw-r--r--src/features/zaps.js19
-rw-r--r--src/i18n.js2
4 files changed, 35 insertions, 16 deletions
diff --git a/src/app.js b/src/app.js
index feb9a6a..9830800 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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.',