From 3a3073366ec6e451ee0b25cc348244230bfd1205 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 18 Mar 2026 18:09:13 -0400 Subject: [PATCH 1/4] feat: move lightning payment filtering to client with full state display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes server-side filtering of payments to only 'success' status. All payments are now passed through with status (success/pending/failed) and expiry fields, and the client derives display state: settled, pending, failed, or expired (pending payments whose expiry timestamp has passed). Updates payments table in all four client apps with new column layout: Date, Amount (sats), Fee, Status, Memo — fixed widths via tableLayout. Closes #236 Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/LightningTab.tsx | 26 ++++++++++++++++--- apps/gatekeeper-client/src/KeymasterUI.jsx | 26 ++++++++++++++++--- apps/keymaster-client/src/KeymasterUI.jsx | 26 ++++++++++++++++--- .../src/components/LightningTab.tsx | 26 ++++++++++++++++--- packages/gatekeeper/src/types.ts | 4 ++- services/drawbridge/server/src/lnbits.ts | 26 ++++++++++++------- 6 files changed, 107 insertions(+), 27 deletions(-) diff --git a/apps/browser-extension/src/components/LightningTab.tsx b/apps/browser-extension/src/components/LightningTab.tsx index 2f6e630b..867933ae 100644 --- a/apps/browser-extension/src/components/LightningTab.tsx +++ b/apps/browser-extension/src/components/LightningTab.tsx @@ -321,11 +321,20 @@ const LightningTab: React.FC = () => { No payments found. )} {!loadingPayments && payments.length > 0 && ( - + + + + + + + + Date - Amount + Amount (sats) + Fee + Status Memo @@ -333,11 +342,20 @@ const LightningTab: React.FC = () => { {payments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; + const displayStatus = p.status === 'success' ? 'settled' + : p.status === 'failed' ? 'failed' + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; + const statusColor = displayStatus === 'settled' ? 'inherit' + : displayStatus === 'failed' ? 'error.main' + : 'text.secondary'; return ( {date} - {p.amount} sats{p.fee > 0 ? ` (fee: ${p.fee})` : ""} - {p.memo || "—"}{p.pending ? " [pending]" : ""} + {p.amount} + {p.fee > 0 ? p.fee : ""} + {displayStatus} + {p.memo || "—"} ); })} diff --git a/apps/gatekeeper-client/src/KeymasterUI.jsx b/apps/gatekeeper-client/src/KeymasterUI.jsx index f864dc21..c8bc93f3 100644 --- a/apps/gatekeeper-client/src/KeymasterUI.jsx +++ b/apps/gatekeeper-client/src/KeymasterUI.jsx @@ -6293,11 +6293,20 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn } {!loadingPayments && lightningPayments.length > 0 && - +
+ + + + + + + Date - Amount + Amount (sats) + Fee + Status Memo @@ -6305,11 +6314,20 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn {lightningPayments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}` : '—'; + const displayStatus = p.status === 'success' ? 'settled' + : p.status === 'failed' ? 'failed' + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; + const statusColor = displayStatus === 'settled' ? 'inherit' + : displayStatus === 'failed' ? 'error.main' + : 'text.secondary'; return ( {date} - {p.amount} sats{p.fee > 0 ? ` (fee: ${p.fee})` : ''} - {p.memo || '—'}{p.pending ? ' [pending]' : ''} + {p.amount} + {p.fee > 0 ? p.fee : ''} + {displayStatus} + {p.memo || '—'} ); })} diff --git a/apps/keymaster-client/src/KeymasterUI.jsx b/apps/keymaster-client/src/KeymasterUI.jsx index f864dc21..c8bc93f3 100644 --- a/apps/keymaster-client/src/KeymasterUI.jsx +++ b/apps/keymaster-client/src/KeymasterUI.jsx @@ -6293,11 +6293,20 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn } {!loadingPayments && lightningPayments.length > 0 && -
+
+ + + + + + + Date - Amount + Amount (sats) + Fee + Status Memo @@ -6305,11 +6314,20 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn {lightningPayments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}` : '—'; + const displayStatus = p.status === 'success' ? 'settled' + : p.status === 'failed' ? 'failed' + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; + const statusColor = displayStatus === 'settled' ? 'inherit' + : displayStatus === 'failed' ? 'error.main' + : 'text.secondary'; return ( {date} - {p.amount} sats{p.fee > 0 ? ` (fee: ${p.fee})` : ''} - {p.memo || '—'}{p.pending ? ' [pending]' : ''} + {p.amount} + {p.fee > 0 ? p.fee : ''} + {displayStatus} + {p.memo || '—'} ); })} diff --git a/apps/react-wallet/src/components/LightningTab.tsx b/apps/react-wallet/src/components/LightningTab.tsx index 2f6e630b..867933ae 100644 --- a/apps/react-wallet/src/components/LightningTab.tsx +++ b/apps/react-wallet/src/components/LightningTab.tsx @@ -321,11 +321,20 @@ const LightningTab: React.FC = () => { No payments found. )} {!loadingPayments && payments.length > 0 && ( - + + + + + + + + - + + + @@ -333,11 +342,20 @@ const LightningTab: React.FC = () => { {payments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; + const displayStatus = p.status === 'success' ? 'settled' + : p.status === 'failed' ? 'failed' + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; + const statusColor = displayStatus === 'settled' ? 'inherit' + : displayStatus === 'failed' ? 'error.main' + : 'text.secondary'; return ( - - + + + + ); })} diff --git a/packages/gatekeeper/src/types.ts b/packages/gatekeeper/src/types.ts index 17901d35..0863c9e8 100644 --- a/packages/gatekeeper/src/types.ts +++ b/packages/gatekeeper/src/types.ts @@ -198,11 +198,13 @@ export interface LightningPaymentStatus { export interface LightningPaymentRecord { paymentHash: string; - amount: number; + amount: number; // positive = incoming, negative = outgoing fee: number; memo: string; time: string; pending: boolean; + status: 'success' | 'pending' | 'failed'; + expiry?: string; // ISO timestamp, invoices only } export interface DrawbridgeInterface extends GatekeeperInterface { diff --git a/services/drawbridge/server/src/lnbits.ts b/services/drawbridge/server/src/lnbits.ts index ec8f715c..09678276 100644 --- a/services/drawbridge/server/src/lnbits.ts +++ b/services/drawbridge/server/src/lnbits.ts @@ -96,21 +96,27 @@ export async function payInvoice( export async function getPayments( url: string, adminKey: string -): Promise> { +): Promise> { try { const response = await axios.get(`${url}/api/v1/payments`, { headers: { 'X-Api-Key': adminKey }, }); return (response.data || []) - .filter((p: any) => p.status === 'success') - .map((p: any) => ({ - paymentHash: p.payment_hash || p.checking_id || '', - amount: Math.floor((p.amount || 0) / 1000), - fee: Math.floor(Math.abs(p.fee || 0) / 1000), - memo: p.memo || '', - time: p.time || '', - pending: false, - })); + .map((p: any) => { + const status = p.status === 'success' ? 'success' + : p.status === 'failed' ? 'failed' + : 'pending'; + return { + paymentHash: p.payment_hash || p.checking_id || '', + amount: Math.floor((p.amount || 0) / 1000), + fee: Math.floor(Math.abs(p.fee || 0) / 1000), + memo: p.memo || '', + time: p.time || '', + pending: status !== 'success', + status, + expiry: p.expiry ?? undefined, + }; + }); } catch (error: any) { throwLnbitsError(error); } From 13d16548f8703254b18fc2669703c87751b5dc51 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 18 Mar 2026 18:36:46 -0400 Subject: [PATCH 2/4] feat: add status filter checkboxes to lightning payments table Adds four checkboxes (settled, pending, failed, expired) next to the Refresh button to filter displayed payments by status in all four client apps. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/LightningTab.tsx | 13 +++++++++++-- apps/gatekeeper-client/src/KeymasterUI.jsx | 16 ++++++++++++---- apps/keymaster-client/src/KeymasterUI.jsx | 16 ++++++++++++---- .../react-wallet/src/components/LightningTab.tsx | 13 +++++++++++-- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/browser-extension/src/components/LightningTab.tsx b/apps/browser-extension/src/components/LightningTab.tsx index 867933ae..cb400580 100644 --- a/apps/browser-extension/src/components/LightningTab.tsx +++ b/apps/browser-extension/src/components/LightningTab.tsx @@ -3,7 +3,9 @@ import { Autocomplete, Box, Button, + Checkbox, CircularProgress, + FormControlLabel, Tab, Tabs, TextField, @@ -52,6 +54,7 @@ const LightningTab: React.FC = () => { // Payments sub-tab const [payments, setPayments] = useState([]); const [loadingPayments, setLoadingPayments] = useState(false); + const [statusFilter, setStatusFilter] = useState({ settled: true, pending: true, failed: true, expired: true }); // Publish state const [isPublished, setIsPublished] = useState(false); @@ -311,10 +314,15 @@ const LightningTab: React.FC = () => { {activeTab === "payments" && ( - + + {(['settled', 'pending', 'failed', 'expired'] as const).map(s => ( + setStatusFilter(f => ({ ...f, [s]: e.target.checked }))} />} /> + ))} {loadingPayments && } {!loadingPayments && payments.length === 0 && ( @@ -342,10 +350,11 @@ const LightningTab: React.FC = () => { {payments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; - const displayStatus = p.status === 'success' ? 'settled' + const displayStatus: 'settled' | 'pending' | 'failed' | 'expired' = p.status === 'success' ? 'settled' : p.status === 'failed' ? 'failed' : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' : 'pending'; + if (!statusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' : 'text.secondary'; diff --git a/apps/gatekeeper-client/src/KeymasterUI.jsx b/apps/gatekeeper-client/src/KeymasterUI.jsx index c8bc93f3..3286efd4 100644 --- a/apps/gatekeeper-client/src/KeymasterUI.jsx +++ b/apps/gatekeeper-client/src/KeymasterUI.jsx @@ -300,6 +300,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn const [zapResult, setZapResult] = useState(null); const [lightningPayments, setLightningPayments] = useState([]); const [loadingPayments, setLoadingPayments] = useState(false); + const [lightningStatusFilter, setLightningStatusFilter] = useState({ settled: true, pending: true, failed: true, expired: true }); const [isPublished, setIsPublished] = useState(false); const [loadingPublishToggle, setLoadingPublishToggle] = useState(false); @@ -6283,10 +6284,16 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn {lightningTab === 'payments' && - -

+ + + {['settled', 'pending', 'failed', 'expired'].map(s => ( + setLightningStatusFilter(f => ({ ...f, [s]: e.target.checked }))} />} /> + ))} + {loadingPayments && Loading...} {!loadingPayments && lightningPayments.length === 0 && No payments found. @@ -6318,6 +6325,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn : p.status === 'failed' ? 'failed' : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' : 'pending'; + if (!lightningStatusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' : 'text.secondary'; diff --git a/apps/keymaster-client/src/KeymasterUI.jsx b/apps/keymaster-client/src/KeymasterUI.jsx index c8bc93f3..3286efd4 100644 --- a/apps/keymaster-client/src/KeymasterUI.jsx +++ b/apps/keymaster-client/src/KeymasterUI.jsx @@ -300,6 +300,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn const [zapResult, setZapResult] = useState(null); const [lightningPayments, setLightningPayments] = useState([]); const [loadingPayments, setLoadingPayments] = useState(false); + const [lightningStatusFilter, setLightningStatusFilter] = useState({ settled: true, pending: true, failed: true, expired: true }); const [isPublished, setIsPublished] = useState(false); const [loadingPublishToggle, setLoadingPublishToggle] = useState(false); @@ -6283,10 +6284,16 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn {lightningTab === 'payments' && - -

+ + + {['settled', 'pending', 'failed', 'expired'].map(s => ( + setLightningStatusFilter(f => ({ ...f, [s]: e.target.checked }))} />} /> + ))} + {loadingPayments && Loading...} {!loadingPayments && lightningPayments.length === 0 && No payments found. @@ -6318,6 +6325,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn : p.status === 'failed' ? 'failed' : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' : 'pending'; + if (!lightningStatusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' : 'text.secondary'; diff --git a/apps/react-wallet/src/components/LightningTab.tsx b/apps/react-wallet/src/components/LightningTab.tsx index 867933ae..cb400580 100644 --- a/apps/react-wallet/src/components/LightningTab.tsx +++ b/apps/react-wallet/src/components/LightningTab.tsx @@ -3,7 +3,9 @@ import { Autocomplete, Box, Button, + Checkbox, CircularProgress, + FormControlLabel, Tab, Tabs, TextField, @@ -52,6 +54,7 @@ const LightningTab: React.FC = () => { // Payments sub-tab const [payments, setPayments] = useState([]); const [loadingPayments, setLoadingPayments] = useState(false); + const [statusFilter, setStatusFilter] = useState({ settled: true, pending: true, failed: true, expired: true }); // Publish state const [isPublished, setIsPublished] = useState(false); @@ -311,10 +314,15 @@ const LightningTab: React.FC = () => { {activeTab === "payments" && ( - + + {(['settled', 'pending', 'failed', 'expired'] as const).map(s => ( + setStatusFilter(f => ({ ...f, [s]: e.target.checked }))} />} /> + ))} {loadingPayments && } {!loadingPayments && payments.length === 0 && ( @@ -342,10 +350,11 @@ const LightningTab: React.FC = () => { {payments.map((p, i) => { const d = p.time ? new Date(p.time) : null; const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; - const displayStatus = p.status === 'success' ? 'settled' + const displayStatus: 'settled' | 'pending' | 'failed' | 'expired' = p.status === 'success' ? 'settled' : p.status === 'failed' ? 'failed' : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' : 'pending'; + if (!statusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' : 'text.secondary'; From d4d64f51d3f0f785b3f4af13b1e2cc9cf5acfb7b Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 18 Mar 2026 18:44:22 -0400 Subject: [PATCH 3/4] fix: correct indentation lint errors in LightningTab and lnbits Co-Authored-By: Claude Sonnet 4.6 --- apps/browser-extension/src/components/LightningTab.tsx | 6 +++--- apps/react-wallet/src/components/LightningTab.tsx | 6 +++--- services/drawbridge/server/src/lnbits.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/browser-extension/src/components/LightningTab.tsx b/apps/browser-extension/src/components/LightningTab.tsx index cb400580..4fc09fe7 100644 --- a/apps/browser-extension/src/components/LightningTab.tsx +++ b/apps/browser-extension/src/components/LightningTab.tsx @@ -352,12 +352,12 @@ const LightningTab: React.FC = () => { const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; const displayStatus: 'settled' | 'pending' | 'failed' | 'expired' = p.status === 'success' ? 'settled' : p.status === 'failed' ? 'failed' - : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' - : 'pending'; + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; if (!statusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' - : 'text.secondary'; + : 'text.secondary'; return (

diff --git a/apps/react-wallet/src/components/LightningTab.tsx b/apps/react-wallet/src/components/LightningTab.tsx index cb400580..4fc09fe7 100644 --- a/apps/react-wallet/src/components/LightningTab.tsx +++ b/apps/react-wallet/src/components/LightningTab.tsx @@ -352,12 +352,12 @@ const LightningTab: React.FC = () => { const date = d ? `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}` : "—"; const displayStatus: 'settled' | 'pending' | 'failed' | 'expired' = p.status === 'success' ? 'settled' : p.status === 'failed' ? 'failed' - : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' - : 'pending'; + : (p.expiry && new Date(p.expiry) < new Date()) ? 'expired' + : 'pending'; if (!statusFilter[displayStatus]) return null; const statusColor = displayStatus === 'settled' ? 'inherit' : displayStatus === 'failed' ? 'error.main' - : 'text.secondary'; + : 'text.secondary'; return ( diff --git a/services/drawbridge/server/src/lnbits.ts b/services/drawbridge/server/src/lnbits.ts index 09678276..2b75a923 100644 --- a/services/drawbridge/server/src/lnbits.ts +++ b/services/drawbridge/server/src/lnbits.ts @@ -105,7 +105,7 @@ export async function getPayments( .map((p: any) => { const status = p.status === 'success' ? 'success' : p.status === 'failed' ? 'failed' - : 'pending'; + : 'pending'; return { paymentHash: p.payment_hash || p.checking_id || '', amount: Math.floor((p.amount || 0) / 1000), From 234e68c698e06517b16b9ceee642f2d6d8e1e207 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 18 Mar 2026 18:53:02 -0400 Subject: [PATCH 4/4] fix: pending field should only be true for pending status, not failed Co-Authored-By: Claude Sonnet 4.6 --- services/drawbridge/server/src/lnbits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/drawbridge/server/src/lnbits.ts b/services/drawbridge/server/src/lnbits.ts index 2b75a923..726e90a7 100644 --- a/services/drawbridge/server/src/lnbits.ts +++ b/services/drawbridge/server/src/lnbits.ts @@ -112,7 +112,7 @@ export async function getPayments( fee: Math.floor(Math.abs(p.fee || 0) / 1000), memo: p.memo || '', time: p.time || '', - pending: status !== 'success', + pending: status === 'pending', status, expiry: p.expiry ?? undefined, };
DateAmountAmount (sats)FeeStatus Memo
{date}{p.amount} sats{p.fee > 0 ? ` (fee: ${p.fee})` : ""}{p.memo || "—"}{p.pending ? " [pending]" : ""}{p.amount}{p.fee > 0 ? p.fee : ""}{displayStatus}{p.memo || "—"}
{date}
{date}