feat: add invoice description fallback for wallet transactions

Add fallback to lightning invoice description when transaction
description is not available. Improves transaction list readability
by showing invoice descriptions instead of generic "Payment" labels.

- Add getInvoiceDescription helper with applesauce caching pattern
- Update TransactionLabel to check invoice description
- Update detail dialog to show invoice description as fallback
- Maintains zap detection logic and UI

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-01-19 13:54:45 +01:00
parent 97dd30f587
commit 3adc9bdfc3
2 changed files with 60 additions and 19 deletions

View File

@@ -52,7 +52,7 @@ import {
} from "@/components/ui/tooltip";
import ConnectWalletDialog from "./ConnectWalletDialog";
import { RelayLink } from "@/components/nostr/RelayLink";
import { parseZapRequest } from "@/lib/wallet-utils";
import { parseZapRequest, getInvoiceDescription } from "@/lib/wallet-utils";
import { Zap } from "lucide-react";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { KindRenderer } from "./nostr/kinds";
@@ -312,14 +312,14 @@ function ZapTransactionDetail({ transaction }: { transaction: Transaction }) {
function TransactionLabel({ transaction }: { transaction: Transaction }) {
const zapInfo = parseZapRequest(transaction);
// Not a zap - use original description or default label
// Not a zap - use original description, invoice description, or default label
if (!zapInfo) {
return (
<span className="text-sm truncate">
{transaction.description ||
(transaction.type === "incoming" ? "Received" : "Payment")}
</span>
);
const description =
transaction.description ||
getInvoiceDescription(transaction) ||
(transaction.type === "incoming" ? "Received" : "Payment");
return <span className="text-sm truncate">{description}</span>;
}
// It's a zap! Show username + message on one line
@@ -1260,17 +1260,24 @@ export default function WalletViewer() {
</div>
<div className="space-y-2">
{selectedTransaction.description &&
!parseZapRequest(selectedTransaction) && (
<div>
<Label className="text-xs text-muted-foreground">
Description
</Label>
<p className="text-sm">
{selectedTransaction.description}
</p>
</div>
)}
{(() => {
const description =
selectedTransaction.description ||
getInvoiceDescription(selectedTransaction);
const isZap = parseZapRequest(selectedTransaction);
return (
description &&
!isZap && (
<div>
<Label className="text-xs text-muted-foreground">
Description
</Label>
<p className="text-sm">{description}</p>
</div>
)
);
})()}
<div>
<Label className="text-xs text-muted-foreground">

View File

@@ -119,3 +119,37 @@ export function parseZapRequest(transaction: {
return null;
});
}
// Symbol for caching invoice description on transaction objects
const InvoiceDescriptionSymbol = Symbol("invoiceDescription");
/**
* Extract the description from a BOLT11 invoice
* Results are cached on the transaction object using applesauce pattern
*
* @param transaction - The transaction object with invoice field
* @returns The invoice description string, or undefined if not available
*/
export function getInvoiceDescription(transaction: {
invoice?: string;
}): string | undefined {
// Use applesauce caching pattern - cache result on transaction object
return getOrComputeCachedValue(transaction, InvoiceDescriptionSymbol, () => {
if (!transaction.invoice) return undefined;
try {
const decoded = decodeBolt11(transaction.invoice);
const descSection = decoded.sections.find(
(s) => s.name === "description",
);
if (descSection && "value" in descSection) {
return String(descSection.value);
}
} catch {
// Invoice decoding failed, ignore
}
return undefined;
});
}