From 642804dedf7c9d0e5bd7c1635fb23d2bac3cbb14 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 3 Sep 2026 20:57:22 -0700 Subject: Reconcile rows earn a real detail page: date, amount, and every consumed coin id — the folded row rides ui state (and nav history) since it isn't a stored movement. Ordinary send details also stop hiding their inputs: 'Coins spent' lists what a payment gathered (up to 24), beside the destination vtxo it minted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr --- src/app.js | 10 ++++++---- src/features/ark.js | 46 +++++++++++++++++++++++++++++++++++++++++----- src/i18n.js | 1 + 3 files changed, 48 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index a48a7d9..7124b40 100644 --- a/src/app.js +++ b/src/app.js @@ -548,7 +548,7 @@ wallet.subscribe(scheduleRender); // views — most Spending payments open one of these, and leaving them out // meant the phone's native Back skipped past the history list entirely // (it landed on whatever tab minted the previous entry). -const NAV_FIELDS = ['screen', 'tab', 'txDetail', 'arkMoveDetail', 'arkExitDetail', 'giftDetail', 'bump', 'giftMode', 'claimStep', 'chatOpen', 'msgView', 'msgCommunity', 'msgPeer', 'profilePk', 'profOverThread', 'settingsPage', 'nameEditOpen', 'noteThread', 'userSearch', 'zapSetup', 'hatShop']; +const NAV_FIELDS = ['screen', 'tab', 'txDetail', 'arkMoveDetail', 'arkReconDetail', 'arkExitDetail', 'giftDetail', 'bump', 'giftMode', 'claimStep', 'chatOpen', 'msgView', 'msgCommunity', 'msgPeer', 'profilePk', 'profOverThread', 'settingsPage', 'nameEditOpen', 'noteThread', 'userSearch', 'zapSetup', 'hatShop']; function navSnapshot() { const s = {}; for (const f of NAV_FIELDS) s[f] = ui[f] ?? null; @@ -3269,7 +3269,7 @@ function setAccountSel(a, dir) { // Selecting an account shows its history right away — home base — and any // open payment detail (which belongs to the account it was opened from) // closes with it. - ui.txDetail = null; ui.arkMoveDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; + ui.txDetail = null; ui.arkMoveDetail = null; ui.arkReconDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; ui.tab = 'history'; render(); } @@ -3412,7 +3412,7 @@ function balanceCard() { try { localStorage.setItem(ACCOUNT_KEY, view); } catch {} // selecting an account lands on its history (home), any open // payment detail belonged to the other account - ui.txDetail = null; ui.arkMoveDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; + ui.txDetail = null; ui.arkMoveDetail = null; ui.arkReconDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; ui.tab = 'history'; render(); } @@ -3510,7 +3510,7 @@ function balanceCard() { if (strip && Date.now() - (strip._dragged || 0) < 300) return; ui.account = v; try { localStorage.setItem(ACCOUNT_KEY, v); } catch {} - ui.txDetail = null; ui.arkMoveDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; + ui.txDetail = null; ui.arkMoveDetail = null; ui.arkReconDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; ui.tab = 'history'; render(); }, @@ -3550,6 +3550,7 @@ function tabsBar() { ui.nostrExportStep = false; // and the exported nostr key ui.txDetail = null; // back to the history list when leaving/returning ui.arkMoveDetail = null; + ui.arkReconDetail = null; ui.giftDetail = null; ui.addrScan = false; // and back to the main Settings, not the address list ui.settingsPage = null; @@ -4768,6 +4769,7 @@ applyDir(); ui.revealShown = false; ui.txDetail = null; ui.arkMoveDetail = null; + ui.arkReconDetail = null; ui.giftDetail = null; ui.addrScan = false; ui.bump = null; diff --git a/src/features/ark.js b/src/features/ark.js index f0b0e9d..e3b42e1 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -983,11 +983,17 @@ export function arkFeature(ctx) { // Deliberately dressed as an ORDINARY Sent row: the user spent their own // money and doesn't need an alarm about which device did it. (The ASP's // status API returns only a spent bit — no spend time or destination — - // so the timestamp is when this wallet found out.) Not tappable: the - // folded row sums several records, so a per-movement detail would show a - // fraction of the amount it advertises. + // so the timestamp is when this wallet found out.) The detail shows the + // hard facts: date, amount, and the coin ids that were consumed. The + // folded row isn't a stored movement, so the whole thing rides ui state. if (m.type === 'reconcile') { - return h('div', { class: 'item' }, + return h('div', { + class: 'item', style: 'cursor:pointer', + onClick: () => { + ui.arkReconDetail = { amountSat: m.amountSat, ts: m.ts, vtxoIds: [...(m.vtxoIds || [])] }; + render(); + }, + }, h('div', { class: 'ico out', html: ARK_MARK(15) }), h('div', { class: 'grow' }, h('div', {}, t('sent')), @@ -1087,6 +1093,30 @@ export function arkFeature(ctx) { h('button', { class: 'btn-ghost btn-block', onClick: () => ctx.goBack(() => { ui.arkExitDetail = null; }) }, t('back'))); } + // One label, N monospace coin ids — the send detail's consumed inputs and + // the reconcile detail's vanished coins share the shape. + const coinListRows = (label, ids) => !ids || !ids.length ? null + : h('div', { class: 'col', style: 'gap:4px' }, + ...ids.map((id, i) => h('div', { class: 'row between', style: 'gap:12px' }, + h('span', { class: 'small muted', style: 'flex-shrink:0' }, i === 0 ? label : ''), + h('span', { class: 'small', style: 'text-align:right;word-break:break-all' }, shortTxid(id))))); + + // Detail for a folded reconcile row: the hard facts only — when this wallet + // learned of it, how much left, and which coins were consumed. + function arkReconDetailView(d) { + const row = (k, v) => h('div', { class: 'row between', style: 'gap:12px' }, + h('span', { class: 'small muted', style: 'flex-shrink:0' }, k), + h('span', { class: 'small', style: 'text-align:right;word-break:break-all' }, v)); + return h('div', { class: 'card col', style: 'gap:10px' }, + h('div', { class: 'row gap6', style: 'align-items:center' }, + h('span', { html: ARK_ICON(18) }), + h('h3', { style: 'margin:0' }, t('sent'))), + h('div', { class: 'amount-neg', style: 'font-size:20px' }, '-' + fmtAmount(d.amountSat) + ' ' + unitLabel()), + row(t('dateLabel'), new Date(d.ts).toLocaleString()), + coinListRows((d.vtxoIds || []).length > 1 ? t('arkVtxoInputs') : t('arkVtxoId'), d.vtxoIds), + h('button', { class: 'btn-ghost btn-block', onClick: () => ctx.goBack(() => { ui.arkReconDetail = null; }) }, t('back'))); + } + function arkMoveDetailView(m) { const incoming = m.type === 'refresh' ? false : !['send', 'offboard', 'exit', 'ln-send'].includes(m.type); const label = m.type === 'receive' ? t('received') : m.type === 'board' ? t('arkBoarded') @@ -1122,6 +1152,8 @@ export function arkFeature(ctx) { })(), m.to ? row(t('arkPayTo'), shortAddr(m.to, 16, 12)) : null, m.vtxoId ? row(t('arkVtxoId'), shortTxid(m.vtxoId)) : null, + // the coins this payment consumed — a send can gather up to 24 inputs + coinListRows(t('arkVtxoInputs'), m.inputIds), // An offboard's fee is two different pockets — the miners' cut for the // on-chain weight and the ASP's service charge (base + lifetime ppm) — // shown apart so neither reads as the other being overpriced. @@ -3005,8 +3037,9 @@ export function arkFeature(ctx) { if (last && last.type === 'reconcile' && last.ts === m.ts) { last.amountSat += m.amountSat; last.count += 1; + if (m.vtxoId) last.vtxoIds.push(m.vtxoId); } else { - out.push({ ...m, count: 1 }); + out.push({ ...m, count: 1, vtxoIds: m.vtxoId ? [m.vtxoId] : [] }); } } return exits.concat(out.map((m) => ({ time: m.ts, render: () => arkHistoryItem(m) }))); @@ -3017,6 +3050,9 @@ export function arkFeature(ctx) { if (a && !['done', 'failed'].includes(a.step)) return arkExitActionDetail(a); ui.arkExitDetail = null; } + // the folded reconcile row isn't a stored movement — its detail rides + // the ui state the row's tap put there (and nav history restores) + if (ui.arkReconDetail && typeof ui.arkReconDetail === 'object') return arkReconDetailView(ui.arkReconDetail); if (!ui.arkMoveDetail) return null; const s = arkStateNow(); const m = s ? (s.movements || []).find((x) => x.id === ui.arkMoveDetail) : null; diff --git a/src/i18n.js b/src/i18n.js index 3ead143..332ee39 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -872,6 +872,7 @@ const STR = { dateLabel: 'Date', detailsLabel: 'Details', arkVtxoId: 'Coin (VTXO)', + arkVtxoInputs: 'Coins spent', lnReceiveIntro: 'Receive over Lightning — paid into this wallet on-chain, non-custodial.', lnReceiveAwaiting: 'Share this invoice. When it’s paid, the sats land here automatically.', lnReceiveWatching: 'Waiting for payment…', -- cgit v1.2.3