diff --git a/src/components/ZapDialog.tsx b/src/components/ZapDialog.tsx index bbf9206..f18ef1f 100644 --- a/src/components/ZapDialog.tsx +++ b/src/components/ZapDialog.tsx @@ -76,8 +76,13 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { // Generate QR code useEffect(() => { + let isCancelled = false; + const generateQR = async () => { - if (!invoice) return; + if (!invoice) { + setQrCodeUrl(''); + return; + } try { const url = await QRCode.toDataURL(invoice.toUpperCase(), { @@ -88,13 +93,22 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { light: '#FFFFFF', }, }); - setQrCodeUrl(url); + + if (!isCancelled) { + setQrCodeUrl(url); + } } catch (err) { - console.error('Failed to generate QR code:', err); + if (!isCancelled) { + console.error('Failed to generate QR code:', err); + } } }; generateQR(); + + return () => { + isCancelled = true; + }; }, [invoice]); const handleCopy = async () => { @@ -122,6 +136,12 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { setInvoice(null); setCopied(false); setQrCodeUrl(''); + } else { + // Clean up state when dialog closes + setAmount(100); + setInvoice(null); + setCopied(false); + setQrCodeUrl(''); } }, [open, setInvoice]); @@ -134,7 +154,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { const ZapContent = () => ( <> {invoice ? ( -
+
{/* Payment amount display */}
{amount} sats
@@ -212,7 +232,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { Open in Lightning Wallet -
+
Scan the QR code or copy the invoice to pay with any Lightning wallet.
@@ -432,9 +452,8 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { Send a Zap - - Zaps are small Bitcoin payments that support the creator of this item. - {' '}If you enjoyed this, consider sending a zap! + + Zaps are small Bitcoin payments that support the creator of this item. If you enjoyed this, consider sending a zap!
@@ -457,13 +476,12 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) { {invoice ? 'Lightning Payment' : 'Send a Zap'} - + {invoice ? ( 'Pay with Bitcoin Lightning Network' ) : ( <> - Zaps are small Bitcoin payments that support the creator of this item. - {' '}If you enjoyed this, consider sending a zap! + Zaps are small Bitcoin payments that support the creator of this item. If you enjoyed this, consider sending a zap! )} diff --git a/src/hooks/useNWC.ts b/src/hooks/useNWC.ts index f11e63a..5a7c370 100644 --- a/src/hooks/useNWC.ts +++ b/src/hooks/useNWC.ts @@ -66,6 +66,7 @@ export function useNWCInternal() { try { // Test the connection by creating an LN client with timeout + let timeoutId: NodeJS.Timeout | undefined; const testPromise = new Promise((resolve, reject) => { try { const client = new LN(parsed.connectionString); @@ -74,10 +75,17 @@ export function useNWCInternal() { reject(error); } }); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Connection test timeout')), 10000); + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Connection test timeout')), 10000); }); - await Promise.race([testPromise, timeoutPromise]) as LN; + + try { + await Promise.race([testPromise, timeoutPromise]) as LN; + if (timeoutId) clearTimeout(timeoutId); + } catch (error) { + if (timeoutId) clearTimeout(timeoutId); + throw error; + } const connection: NWCConnection = { connectionString: parsed.connectionString, @@ -149,7 +157,7 @@ export function useNWCInternal() { setActiveConnection(connections[0].connectionString); return connections[0]; } - + if (!activeConnection) return null; const found = connections.find(c => c.connectionString === activeConnection); @@ -175,14 +183,22 @@ export function useNWCInternal() { } try { - // Add timeout to prevent hanging - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15000); + // Add timeout to prevent hanging with proper cleanup + let timeoutId: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15000); }); const paymentPromise = client.pay(invoice); - const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string }; - return response; + + try { + const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string }; + if (timeoutId) clearTimeout(timeoutId); + return response; + } catch (error) { + if (timeoutId) clearTimeout(timeoutId); + throw error; + } } catch (error) { console.error('NWC payment failed:', error); @@ -222,6 +238,7 @@ export function useNWCInternal() { try { // Create a fresh client for testing + let timeoutId: NodeJS.Timeout | undefined; const testPromise = new Promise((resolve, reject) => { try { const client = new LN(connection.connectionString); @@ -231,10 +248,17 @@ export function useNWCInternal() { } }); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Connection test timeout')), 5000); + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Connection test timeout')), 5000); }); - await Promise.race([testPromise, timeoutPromise]); + + try { + await Promise.race([testPromise, timeoutPromise]); + if (timeoutId) clearTimeout(timeoutId); + } catch (error) { + if (timeoutId) clearTimeout(timeoutId); + throw error; + } return true; } catch (error) { diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts index 36840df..9589449 100644 --- a/src/hooks/useZaps.ts +++ b/src/hooks/useZaps.ts @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useAppContext } from '@/hooks/useAppContext'; @@ -32,10 +32,21 @@ export function useZaps( const [isZapping, setIsZapping] = useState(false); const [invoice, setInvoice] = useState(null); + // Cleanup state when component unmounts + useEffect(() => { + return () => { + setIsZapping(false); + setInvoice(null); + }; + }, []); + const { data: zapEvents, ...query } = useQuery({ queryKey: ['zaps', actualTarget?.id], staleTime: 30000, // 30 seconds - refetchInterval: 60000, // Refetch every minute to catch new zaps + refetchInterval: (query) => { + // Only refetch if the query is currently being observed (component is mounted) + return query.getObserversCount() > 0 ? 60000 : false; + }, queryFn: async (c) => { if (!actualTarget) return []; @@ -64,7 +75,7 @@ export function useZaps( // Process zap events into simple counts and totals const { zapCount, totalSats, zaps } = useMemo(() => { - if (!zapEvents || !actualTarget) { + if (!zapEvents || !Array.isArray(zapEvents) || !actualTarget) { return { zapCount: 0, totalSats: 0, zaps: [] }; }