diff --git a/src/components/wallet/NwcWalletCard.tsx b/src/components/wallet/NwcWalletCard.tsx new file mode 100644 index 0000000..829ec6e --- /dev/null +++ b/src/components/wallet/NwcWalletCard.tsx @@ -0,0 +1,223 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { useNWC } from '@/hooks/useNWCContext'; +import { useToast } from '@/hooks/useToast'; +import { getSatoshisFromBolt11 } from '@/lib/bolt11'; +import { cn } from '@/lib/utils'; +import { + ArrowDownToLine, + ArrowUpFromLine, + Check, + Copy, + Loader2, + RefreshCw, + Zap, +} from 'lucide-react'; + +/** + * Lightning wallet over Nostr Wallet Connect (NIP-47). + * + * The Wallet page was Cashu-only and showed "App only" on web, so anyone who + * connected an NWC wallet (Alby, Coinos, …) saw no balance, send or receive. + * This card surfaces the connected wallet: balance, make-invoice (receive) and + * pay-invoice (send). It renders nothing when no NWC connection is active. + */ +export function NwcWalletCard() { + const { getActiveConnection, getBalance, makeInvoice, sendPayment } = useNWC(); + const { toast } = useToast(); + const connection = getActiveConnection(); + + const [balance, setBalance] = useState(null); + const [loadingBalance, setLoadingBalance] = useState(false); + + const [receiveAmount, setReceiveAmount] = useState(''); + const [creating, setCreating] = useState(false); + const [invoice, setInvoice] = useState(null); + const [qr, setQr] = useState(null); + const [copied, setCopied] = useState(false); + + const [payInput, setPayInput] = useState(''); + const [paying, setPaying] = useState(false); + + const refreshBalance = useCallback(async () => { + if (!connection) return; + setLoadingBalance(true); + try { + setBalance(await getBalance(connection)); + } catch (e) { + toast({ + title: 'Could not read balance', + description: e instanceof Error ? e.message : 'Your wallet did not respond.', + variant: 'destructive', + }); + } finally { + setLoadingBalance(false); + } + }, [connection, getBalance, toast]); + + useEffect(() => { + void refreshBalance(); + }, [refreshBalance]); + + const handleCreateInvoice = async () => { + if (!connection) return; + const sats = Number(receiveAmount); + if (!Number.isFinite(sats) || sats <= 0) { + toast({ title: 'Enter an amount in sats', variant: 'destructive' }); + return; + } + setCreating(true); + try { + const inv = await makeInvoice(connection, sats, 'SkateSpots'); + setInvoice(inv); + const { default: QRCode } = await import('qrcode'); + setQr(await QRCode.toDataURL(inv.toUpperCase(), { width: 256, margin: 1 })); + } catch (e) { + toast({ + title: 'Could not create invoice', + description: e instanceof Error ? e.message : 'Try again in a moment.', + variant: 'destructive', + }); + } finally { + setCreating(false); + } + }; + + const handlePay = async () => { + if (!connection) return; + const inv = payInput.trim(); + if (!inv) return; + setPaying(true); + try { + await sendPayment(connection, inv); + const sats = (() => { try { return getSatoshisFromBolt11(inv); } catch { return 0; } })(); + toast({ title: 'Payment sent', description: sats > 0 ? `Paid ${sats.toLocaleString()} sats.` : 'Paid.' }); + setPayInput(''); + void refreshBalance(); + } catch (e) { + toast({ + title: 'Payment failed', + description: e instanceof Error ? e.message : 'The wallet rejected the payment.', + variant: 'destructive', + }); + } finally { + setPaying(false); + } + }; + + const copyInvoice = async () => { + if (!invoice) return; + try { + await navigator.clipboard.writeText(invoice); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + /* ignore */ + } + }; + + if (!connection) return null; + + const paySats = (() => { + try { return payInput.trim() ? getSatoshisFromBolt11(payInput.trim()) : 0; } catch { return 0; } + })(); + + return ( + + + + + Lightning Wallet + +

+ {connection.alias || 'NWC wallet'} · NIP-47 +

+
+ + {/* Balance */} +
+
+

Balance

+

+ {loadingBalance && balance === null ? '…' : balance === null ? '—' : balance.toLocaleString()} + sats +

+
+ +
+ + {/* Receive */} +
+

+ Receive +

+
+ setReceiveAmount(e.target.value)} + inputMode="numeric" + placeholder="Amount in sats" + className="h-11 border-gray-700 bg-gray-900 text-white" + /> + +
+ {invoice && ( +
+ {qr && Lightning invoice QR code} +

{invoice}

+ +
+ )} +
+ + {/* Send */} +
+

+ Send +

+