diff --git a/src/components/ZapWindow.tsx b/src/components/ZapWindow.tsx index c7219a0..1708679 100644 --- a/src/components/ZapWindow.tsx +++ b/src/components/ZapWindow.tsx @@ -133,7 +133,7 @@ export function ZapWindow({ // Poll for payment completion via LNURL verify endpoint const PAYMENT_CHECK_INTERVAL = 30000; // Check every 30 seconds useEffect(() => { - if (!verifyUrl || !showQrDialog || isPaid) return; + if (!verifyUrl || !showQrDialog || isPaid || !isOnline) return; const checkPayment = async () => { // Check if invoice has expired @@ -148,6 +148,7 @@ export function ZapWindow({ setCheckingPayment(true); try { const result = await verifyPayment(verifyUrl); + setLastChecked(Math.floor(Date.now() / 1000)); // If payment is settled, mark as paid and show success if (result.status === "OK" && result.settled) { @@ -161,6 +162,7 @@ export function ZapWindow({ } catch (error) { // Silently ignore errors - will retry on next interval console.debug("Payment verification check failed:", error); + setLastChecked(Math.floor(Date.now() / 1000)); } finally { setCheckingPayment(false); } @@ -176,11 +178,45 @@ export function ZapWindow({ showQrDialog, isPaid, invoiceExpiry, + isOnline, selectedAmount, customAmount, recipientName, ]); + // Network online/offline detection + useEffect(() => { + const handleOnline = () => setIsOnline(true); + const handleOffline = () => setIsOnline(false); + + window.addEventListener("online", handleOnline); + window.addEventListener("offline", handleOffline); + + return () => { + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", handleOffline); + }; + }, []); + + // Countdown timer for invoice expiry + useEffect(() => { + if (!invoiceExpiry || !showQrDialog) return; + + const updateCountdown = () => { + const nowSeconds = Math.floor(Date.now() / 1000); + const remaining = invoiceExpiry - nowSeconds; + setTimeRemaining(remaining > 0 ? remaining : 0); + }; + + // Update immediately + updateCountdown(); + + // Update every second + const intervalId = setInterval(updateCountdown, 1000); + + return () => clearInterval(intervalId); + }, [invoiceExpiry, showQrDialog]); + // Cache LNURL data for recipient's Lightning address const { data: lnurlData } = useLnurlCache(recipientProfile?.lud16); @@ -198,6 +234,9 @@ export function ZapWindow({ const [verifyUrl, setVerifyUrl] = useState(""); const [checkingPayment, setCheckingPayment] = useState(false); const [invoiceExpiry, setInvoiceExpiry] = useState(null); // Unix timestamp when invoice expires + const [lastChecked, setLastChecked] = useState(null); // Unix timestamp of last check + const [timeRemaining, setTimeRemaining] = useState(null); // Seconds until expiry + const [isOnline, setIsOnline] = useState(navigator.onLine); // Editor ref and search functions const editorRef = useRef(null); @@ -553,6 +592,52 @@ export function ZapWindow({ setShowLogin(true); }; + // Manual payment verification check + const handleManualCheck = async () => { + if (!verifyUrl || checkingPayment || isInvoiceExpired) return; + + setCheckingPayment(true); + try { + const result = await verifyPayment(verifyUrl); + setLastChecked(Math.floor(Date.now() / 1000)); + + if (result.status === "OK" && result.settled) { + setIsPaid(true); + setShowQrDialog(false); + const amount = selectedAmount || parseInt(customAmount); + toast.success( + `⚡ Payment received! Zapped ${amount} sats to ${recipientName}!`, + ); + } else { + toast.info("Payment not yet received"); + } + } catch (error) { + console.error("Manual payment check failed:", error); + toast.error("Failed to check payment status"); + setLastChecked(Math.floor(Date.now() / 1000)); + } finally { + setCheckingPayment(false); + } + }; + + // Format time remaining as MM:SS + const formatTimeRemaining = (seconds: number): string => { + if (seconds <= 0) return "Expired"; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, "0")}`; + }; + + // Format last checked timestamp + const formatLastChecked = (timestamp: number | null): string => { + if (!timestamp) return ""; + const secondsAgo = Math.floor(Date.now() / 1000) - timestamp; + if (secondsAgo < 5) return "just now"; + if (secondsAgo < 60) return `${secondsAgo}s ago`; + const minsAgo = Math.floor(secondsAgo / 60); + return `${minsAgo}m ago`; + }; + // Retry wallet payment const handleRetryWallet = async () => { if (!invoice || !wallet) return; @@ -612,7 +697,7 @@ export function ZapWindow({ {/* QR Code */} {qrCodeUrl && (
@@ -621,9 +706,57 @@ export function ZapWindow({ alt="Lightning Invoice QR Code" className="w-64 h-64" /> + {/* Checking animation - subtle pulse on QR border */} + {checkingPayment && verifyUrl && !isInvoiceExpired && ( +
+ )}
)} + {/* Status indicators */} +
+ {/* Expiry countdown */} + {timeRemaining !== null && ( +
+ {isInvoiceExpired + ? "⏰ Invoice expired" + : `Expires in ${formatTimeRemaining(timeRemaining)}`} +
+ )} + + {/* Verification status */} + {verifyUrl && !isInvoiceExpired && ( +
+ {!isOnline ? ( + <> + + Offline - verification paused + + ) : checkingPayment ? ( + <> + + Checking... + + ) : lastChecked ? ( + <> + + + Last checked {formatLastChecked(lastChecked)} + + + ) : null} +
+ )} +
+ {/* Amount Preview */} {(selectedAmount || customAmount) && (
@@ -681,11 +814,34 @@ export function ZapWindow({ )} + {/* Manual check button if verify URL available */} + {verifyUrl && !isInvoiceExpired && ( + + )} + {/* Always show option to open in external wallet */}