import { finalizeEvent, nip19, } from "nostr-tools"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" import { ReloadIcon, CheckCircledIcon } from "@radix-ui/react-icons"; import ZapButtonList from "./ZapButtonList"; import { Input } from "./ui/input"; import { useNostr, useNostrEvents, useProfile } from "nostr-react"; import { useEffect, useState, useRef } from "react"; import QRCode from "react-qr-code"; import Link from "next/link"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { signEvent } from "@/utils/utils"; import { nwc } from "@getalby/sdk"; import { Sparkles, Zap } from "lucide-react"; // NWC connection storage key (same as used in NostrWalletConnect.tsx) const NWC_STORAGE_KEY = "lumina-nwc-connection"; export default function ZapButton({ event }: { event: any }) { let loginType = ''; if (typeof window !== 'undefined') { loginType = window.localStorage.getItem("loginType") ?? ''; } const { connectedRelays } = useNostr(); const { events, isLoading } = useNostrEvents({ filter: { '#e': [event.id], kinds: [9735], }, }); const [lnurlPayInfo, setLnurlPayInfo] = useState(null); const [invoice, setInvoice] = useState(""); const [customAmount, setCustomAmount] = useState("21"); const [isProcessing, setIsProcessing] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [paymentComplete, setPaymentComplete] = useState(false); const { publish } = useNostr(); // NWC state const [useNwc, setUseNwc] = useState(false); const [nwcClient, setNwcClient] = useState(null); const [nwcPaymentStatus, setNwcPaymentStatus] = useState(""); const [paymentPreimage, setPaymentPreimage] = useState(""); // Store the initial count of zap receipts when an invoice is generated const invoiceEventsCountRef = useRef(0); // Effect to check for new zap receipts when showing an invoice useEffect(() => { if (invoice) { // Store the current count of zap receipts when invoice is generated invoiceEventsCountRef.current = events.length; setPaymentComplete(false); } }, [invoice]); // Effect to detect new zap receipts after invoice is shown useEffect(() => { if (invoice && events.length > invoiceEventsCountRef.current) { // Filter events to find new zap receipts related to current invoice const newEvents = events.slice(invoiceEventsCountRef.current); // Check if any new events contain the current invoice const relevantEvents = newEvents.filter(zapEvent => { // Look for bolt11 tag containing the invoice return zapEvent.tags.some(tag => tag[0] === 'bolt11' && invoice.includes(tag[1].substring(0, 50)) ); }); if (relevantEvents.length > 0) { setPaymentComplete(true); } } }, [events, invoice]); // Initialize NWC client on first render useEffect(() => { initializeNwc(); }, []); // Initialize NWC client from localStorage const initializeNwc = async () => { if (typeof window === 'undefined') return; const connectionUrl = localStorage.getItem(NWC_STORAGE_KEY); if (!connectionUrl) return; try { const client = new nwc.NWCClient({ nostrWalletConnectUrl: connectionUrl, }); // Test the connection by getting wallet info await client.getInfo(); setNwcClient(client); setUseNwc(true); } catch (error) { console.error("Failed to initialize NWC client:", error); // Don't show error to user here as this is just an initialization check } }; const { data: userData } = useProfile({ pubkey: event.pubkey, }); let sats = 0; var lightningPayReq = require('bolt11'); events.forEach((event) => { event.tags.forEach((tag) => { if (tag[0] === 'bolt11') { try { lightningPayReq.decode(tag[1]); let decoded = lightningPayReq.decode(tag[1]); sats = sats + decoded.satoshis; } catch (e) { console.error("Error decoding bolt11 tag:", e); console.log(tag[1]); return null; } } }); }); const fetchLnurlInfo = async () => { setErrorMessage(""); if (!userData?.lud06 && !userData?.lud16) { setErrorMessage("This user doesn't have a Lightning address configured"); return; } try { setIsProcessing(true); let lnurl; if (userData.lud06) { lnurl = userData.lud06; } else if (userData.lud16) { const [name, domain] = userData.lud16.split('@'); if (!name || !domain) { throw new Error("Invalid lightning address format"); } const url = `https://${domain}/.well-known/lnurlp/${name}`; lnurl = url; } else { throw new Error("No lightning address found in profile"); } if (lnurl.toLowerCase().startsWith('lnurl')) { try { lnurl = bech32ToUrl(lnurl); } catch (e) { console.error("Error decoding LNURL:", e); throw new Error("Invalid LNURL format"); } } try { // Add a timeout to the fetch request const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch(lnurl, { signal: controller.signal, headers: { 'Accept': 'application/json', } }); clearTimeout(timeoutId); // Check content type to ensure it's JSON const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { console.error("Invalid content type:", contentType); throw new Error("Lightning service returned invalid content type (expected JSON)"); } // Parse response as JSON const data = await response.json(); if (!response.ok) { throw new Error(`Error fetching LNURL info: ${data.reason || response.statusText || 'Unknown error'}`); } if (!data.callback) { throw new Error("Invalid LNURL response: missing callback URL"); } if (!data.allowsNostr) { setErrorMessage("This Lightning address doesn't support Nostr zaps"); setIsProcessing(false); return; } setLnurlPayInfo(data); } catch (error) { if (typeof error === "object" && error !== null && "name" in error && typeof (error as any).name === "string") { if ((error as any).name === 'AbortError') { throw new Error("Request timed out - Lightning service not responding"); } else if ((error as any).name === 'SyntaxError') { throw new Error("Lightning service returned invalid data format"); } } if (typeof error === "object" && error !== null && "message" in error && typeof (error as any).message === "string" && (error as any).message.includes('NetworkError')) { throw new Error("Network error: CORS issue or service unavailable"); } else { throw error; } } } catch (error) { console.error("Error fetching LNURL info:", error); setErrorMessage(error instanceof Error ? error.message : "Failed to fetch Lightning payment information"); } finally { setIsProcessing(false); } }; const createZapRequest = async (amount: number) => { if (!lnurlPayInfo || !lnurlPayInfo.callback || !lnurlPayInfo.allowsNostr || !lnurlPayInfo.nostrPubkey) { setErrorMessage("Invalid Lightning payment information"); return; } if (amount < lnurlPayInfo.minSendable || amount > lnurlPayInfo.maxSendable) { setErrorMessage(`Amount must be between ${lnurlPayInfo.minSendable / 1000} and ${lnurlPayInfo.maxSendable / 1000} sats`); return; } try { setIsProcessing(true); setErrorMessage(""); let senderPubkey = ''; if (typeof window !== 'undefined') { senderPubkey = window.localStorage.getItem('pubkey') ?? ''; } if (!senderPubkey) { setErrorMessage("You need to be logged in to send zaps"); setIsProcessing(false); return; } let zapRequestEvent = { kind: 9734, content: "", tags: [ ["relays", ...connectedRelays.map((relay) => relay.url)], ["amount", amount.toString()], ["p", event.pubkey], ["e", event.id], ], created_at: Math.floor(Date.now() / 1000), pubkey: senderPubkey, id: "", // Add placeholder for id sig: "", // Add placeholder for sig }; let params = new URLSearchParams(); params.append('amount', amount.toString()); if (userData?.lud06) { zapRequestEvent.tags.push(["lnurl", userData.lud06]); } const signedZapRequest = await signEvent(loginType, zapRequestEvent); params.append('nostr', JSON.stringify(signedZapRequest)); if (userData?.lud06) { params.append('lnurl', userData.lud06); } let callbackUrl = `${lnurlPayInfo.callback}?${params.toString()}`; const invoiceResponse = await fetch(callbackUrl); const invoiceData = await invoiceResponse.json(); if (!invoiceResponse.ok || !invoiceData.pr) { throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); } setInvoice(invoiceData.pr); } catch (error) { console.error("Error creating zap request:", error); setErrorMessage(error instanceof Error ? error.message : "Failed to create zap request"); } finally { setIsProcessing(false); } }; // Handle sending a zap using NWC const handleNwcPayment = async (amountSats: number) => { if (!nwcClient) { setErrorMessage("NWC client not initialized"); return; } if (!userData?.lud16 && !userData?.lud06) { setErrorMessage("This user doesn't have a Lightning address configured"); return; } setIsProcessing(true); setNwcPaymentStatus("Preparing payment..."); setPaymentComplete(false); setErrorMessage(""); try { let senderPubkey = ''; if (typeof window !== 'undefined') { senderPubkey = window.localStorage.getItem('pubkey') ?? ''; } if (!senderPubkey) { setErrorMessage("You need to be logged in to send zaps"); return; } // Create zap request event let zapRequestEvent = { kind: 9734, content: "", tags: [ ["relays", ...connectedRelays.map((relay) => relay.url)], ["amount", (amountSats * 1000).toString()], ["p", event.pubkey], ["e", event.id], ], created_at: Math.floor(Date.now() / 1000), pubkey: senderPubkey, id: "", sig: "", }; // Sign the zap request setNwcPaymentStatus("Signing zap request..."); const signedZapRequest = await signEvent(loginType, zapRequestEvent); // Get LNURL payment info for the Lightning address let recipient = ''; if (userData?.lud16) { recipient = userData.lud16; } else if (userData?.lud06) { recipient = userData.lud06; } if (!recipient) { throw new Error("Could not determine recipient lightning address"); } setNwcPaymentStatus("Getting invoice from recipient..."); // Get LNURL payment info and invoice let lnurl; let invoice; if (recipient.includes('@')) { // Handle Lightning Address (lud16) const [name, domain] = recipient.split('@'); const url = `https://${domain}/.well-known/lnurlp/${name}`; // Get LNURL callback info const lnurlResponse = await fetch(url); const lnurlData = await lnurlResponse.json(); if (!lnurlData.callback || !lnurlData.allowsNostr) { throw new Error("Lightning address doesn't support Nostr zaps"); } // Get invoice with zap request const params = new URLSearchParams(); params.append('amount', (amountSats * 1000).toString()); params.append('nostr', JSON.stringify(signedZapRequest)); const callbackUrl = `${lnurlData.callback}?${params.toString()}`; const invoiceResponse = await fetch(callbackUrl); const invoiceData = await invoiceResponse.json(); if (!invoiceResponse.ok || !invoiceData.pr) { throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); } invoice = invoiceData.pr; } else { // Handle LNURL directly (lud06) // Decode if needed if (recipient.toLowerCase().startsWith('lnurl')) { try { recipient = bech32ToUrl(recipient); } catch (e) { throw new Error("Invalid LNURL format"); } } // Get LNURL callback info const lnurlResponse = await fetch(recipient); const lnurlData = await lnurlResponse.json(); if (!lnurlData.callback || !lnurlData.allowsNostr) { throw new Error("LNURL doesn't support Nostr zaps"); } // Get invoice with zap request const params = new URLSearchParams(); params.append('amount', (amountSats * 1000).toString()); params.append('nostr', JSON.stringify(signedZapRequest)); const callbackUrl = `${lnurlData.callback}?${params.toString()}`; const invoiceResponse = await fetch(callbackUrl); const invoiceData = await invoiceResponse.json(); if (!invoiceResponse.ok || !invoiceData.pr) { throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); } invoice = invoiceData.pr; } // Now pay the invoice using NWC setNwcPaymentStatus("Sending payment via NWC..."); const paymentResponse = await nwcClient.payInvoice({ invoice: invoice }); if (paymentResponse.preimage) { setPaymentPreimage(paymentResponse.preimage); setPaymentComplete(true); setNwcPaymentStatus("Payment sent successfully!"); } else { throw new Error("Payment failed - no preimage returned"); } } catch (error) { console.error("NWC payment error:", error); setNwcPaymentStatus(""); setErrorMessage(error instanceof Error ? error.message : "Payment failed"); } finally { setIsProcessing(false); } }; const handleZap = async (amountSats: number) => { if (useNwc && nwcClient) { await handleNwcPayment(amountSats); } else { if (!lnurlPayInfo) { await fetchLnurlInfo(); } createZapRequest(amountSats * 1000); } }; const handleCustomZap = async () => { const amount = parseInt(customAmount, 10); if (isNaN(amount) || amount <= 0) { setErrorMessage("Please enter a valid amount"); return; } if (useNwc && nwcClient) { await handleNwcPayment(amount); } else { if (!lnurlPayInfo) { await fetchLnurlInfo(); } createZapRequest(amount * 1000); } }; const bech32ToUrl = (lnurl: string): string => { try { const decoded = nip19.decode(lnurl); return decoded.data as string; } catch (e) { return lnurl; } }; const handleOpenDrawer = () => { if (!useNwc) { fetchLnurlInfo(); } }; const handleCloseDrawer = () => { setInvoice(""); setErrorMessage(""); setIsProcessing(false); setPaymentComplete(false); setNwcPaymentStatus(""); }; const checkPaymentStatus = async () => { setIsProcessing(true); try { // Force a re-fetch of zap receipt events const eventFilter = { '#e': [event.id], kinds: [9735], }; // Manually check relays for new zap events const zapPromises = connectedRelays.map(async (relay) => { return new Promise(async (resolve) => { const timeout = setTimeout(() => resolve([]), 3000); // 3 second timeout try { const sub = relay.sub([eventFilter]); const events: any[] = []; sub.on('event', (event) => { // Check if this event contains the current invoice const hasBolt11 = event.tags.some(tag => tag[0] === 'bolt11' && invoice.includes(tag[1].substring(0, 50)) ); if (hasBolt11) { events.push(event); } }); sub.on('eose', () => { clearTimeout(timeout); resolve(events); sub.unsub(); }); } catch (error) { clearTimeout(timeout); resolve([]); } }); }); const zapEventsArrays = await Promise.all(zapPromises); const newZapEvents = zapEventsArrays.flat(); if (newZapEvents.length > 0) { setPaymentComplete(true); } } catch (error) { console.error("Error checking payment status:", error); } finally { setIsProcessing(false); } }; return ( open ? handleOpenDrawer() : handleCloseDrawer()}> {isLoading ? ( ) : ( )} {errorMessage && ( {/* */} Error {errorMessage} )} {/* NWC Payment Mode */} {useNwc && !invoice && (
{!paymentComplete && ( NWC Enabled Payments will be sent directly through your connected NWC wallet )} {nwcPaymentStatus && !paymentComplete && ( {isProcessing ? ( ) : ( )} {nwcPaymentStatus} )} {paymentComplete && (

Payment Complete!

{/* {paymentPreimage && (

Payment Preimage:

{paymentPreimage}

)} */}
Zap sent successfully!
)} {(!paymentComplete || !isProcessing) && !paymentComplete && (
setCustomAmount(e.target.value)} disabled={isProcessing} />
)} {!paymentComplete &&
} {!paymentComplete && }
)} {/* Original LNURL Payment Flow */} {(!useNwc || invoice) && ( <> {invoice ? (
{paymentComplete ? (

Payment Complete!

) : (
)} {!paymentComplete && ( <>

Scan this QR code with a Lightning wallet to pay the invoice

{invoice}
)} {paymentComplete ? (
Zap sent successfully!
) : ( )}
) : ( <>
setCustomAmount(e.target.value)} disabled={isProcessing} />

)} )}
); }