diff --git a/AGENTS.md b/AGENTS.md index 6164255..383c895 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,14 @@ session with **no other memory of this project**. So: NIP.md). `creq`/`clink` are recognized but **not payable yet**. This is the **Numo/BTCPay-derived** "pay the seller in-app" rail; the request format is ours (offer tags), not a new kind. + - **Buy with sats — fixed price, no negotiation (2026-10-01):** a + `price_type=fixed` listing shows a direct **Buy with sats** button in + `ListingTradeActions` (shared by card + detail). The price is resolved to + sats — **SATS** directly, **USD** via `useUsdToSats` (live rate); other + currencies fall back to the offer/contact flow — and paid through the same + `PayWithSats` sheet (NWC → WebLN → Cashu melt, falling back to the seller's + `lud16`). ⚠️ The sheet is **mounted only while open** so its wallet hooks + don't run on every grid card. - **Cashu escrow** (marketplace): time-locked **P2PK** (lock to seller + buyer refund key + locktime) — see **`ESCROW.md`** and `src/lib/cashuEscrow.ts` (unit-tested). Protocol: `cashu_token` on the `31872`; each user advertises a diff --git a/src/components/ListingTradeActions.tsx b/src/components/ListingTradeActions.tsx index ed38732..db41b4d 100644 --- a/src/components/ListingTradeActions.tsx +++ b/src/components/ListingTradeActions.tsx @@ -14,8 +14,10 @@ import { import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow'; import { EscrowRequestDialog } from '@/components/EscrowRequestDialog'; import { EscrowPanel } from '@/components/EscrowPanel'; +import { PayWithSats } from '@/components/PayWithSats'; +import { useUsdToSats } from '@/hooks/useExchangeRate'; import { parseEscrowToken } from '@/lib/cashuEscrow'; -import { MessageCircle, Loader2, CheckCircle, Lock, Send, RotateCcw } from 'lucide-react'; +import { MessageCircle, Loader2, CheckCircle, Lock, Send, RotateCcw, Zap } from 'lucide-react'; import type { MarketplaceItem } from '@/lib/types'; /** @@ -41,11 +43,29 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { const escrow = useCashuEscrow(); const [showEscrow, setShowEscrow] = useState(false); + const [showPay, setShowPay] = useState(false); const isOwnListing = user?.pubkey === item.pubkey; const isSold = item.status === 'sold'; const isWTB = item.listingType === 'wtb'; + // "Buy with sats" for fixed-price listings: resolve the price to sats — a SATS + // price directly, a USD price via the live rate. Other currencies fall back to + // the offer/contact flow. + const priceCurrency = item.price?.currency?.toUpperCase(); + const priceAmount = Number(item.price?.amount); + const usdSats = useUsdToSats( + priceCurrency === 'USD' && Number.isFinite(priceAmount) ? priceAmount : null, + ); + const buyAmountSats = + item.priceType === 'fixed' && item.price && Number.isFinite(priceAmount) + ? priceCurrency === 'SATS' || priceCurrency === 'SAT' + ? priceAmount + : priceCurrency === 'USD' + ? usdSats + : null + : null; + const sellerNamedMe = !!user && !isOwnListing && @@ -56,6 +76,16 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { const iConfirmedTrade = !!trade?.iConfirmed; const iRequestedConfirmation = !!requestState?.iRequested; + const canBuy = + !!user && + !isOwnListing && + !isWTB && + !isSold && + !iConfirmedTrade && + item.priceType === 'fixed' && + typeof buyAmountSats === 'number' && + buyAmountSats > 0; + const sellerEscrowPubkey = typeof (metadata as Record | undefined)?.cashu_escrow === 'string' ? ((metadata as Record).cashu_escrow as string) @@ -194,6 +224,18 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { )} + {/* Buyer, fixed price: pay directly — no negotiation needed */} + {canBuy && ( + + )} + {/* Contact button */}