mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-08 22:46:49 +02:00
782 lines
32 KiB
TypeScript
782 lines
32 KiB
TypeScript
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<any>(null);
|
||
const [invoice, setInvoice] = useState<string>("");
|
||
const [customAmount, setCustomAmount] = useState<string>("21");
|
||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||
const [paymentComplete, setPaymentComplete] = useState<boolean>(false);
|
||
const { publish } = useNostr();
|
||
|
||
// NWC state
|
||
const [useNwc, setUseNwc] = useState<boolean>(false);
|
||
const [nwcClient, setNwcClient] = useState<nwc.NWCClient | null>(null);
|
||
const [nwcPaymentStatus, setNwcPaymentStatus] = useState<string>("");
|
||
const [paymentPreimage, setPaymentPreimage] = useState<string>("");
|
||
|
||
// Store the initial count of zap receipts when an invoice is generated
|
||
const invoiceEventsCountRef = useRef<number>(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 (
|
||
<Drawer onOpenChange={(open) => open ? handleOpenDrawer() : handleCloseDrawer()}>
|
||
<DrawerTrigger asChild>
|
||
{isLoading ? (
|
||
<Button variant="outline"><ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> ⚡️</Button>
|
||
) : (
|
||
<Button variant="outline">{sats} sats ⚡️</Button>
|
||
)}
|
||
</DrawerTrigger>
|
||
<DrawerContent>
|
||
{errorMessage && (
|
||
<Alert variant={"destructive"} className="mx-4 mt-4">
|
||
{/* <AlertCircleIcon className="h-4 w-4" /> */}
|
||
<AlertTitle>Error</AlertTitle>
|
||
<AlertDescription>{errorMessage}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* NWC Payment Mode */}
|
||
{useNwc && !invoice && (
|
||
<div className="px-4 pt-4">
|
||
{!paymentComplete && (
|
||
<Alert variant="default" className="mb-4 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300">
|
||
<Sparkles className="h-4 w-4" />
|
||
<AlertTitle>NWC Enabled</AlertTitle>
|
||
<AlertDescription>Payments will be sent directly through your connected NWC wallet</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{nwcPaymentStatus && !paymentComplete && (
|
||
<Alert variant="default" className="mb-4 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300">
|
||
{isProcessing ? (
|
||
<ReloadIcon className="h-4 w-4 animate-spin" />
|
||
) : (
|
||
<Zap className="h-4 w-4" />
|
||
)}
|
||
<AlertDescription>{nwcPaymentStatus}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{paymentComplete && (
|
||
<div className="flex flex-col items-center mb-6">
|
||
<div className="flex flex-col items-center justify-center mb-4 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 w-[200px] h-[200px]">
|
||
<CheckCircledIcon className="h-24 w-24 text-green-500" />
|
||
<p className="text-lg font-semibold text-green-500 mt-4 text-center">
|
||
Payment Complete!
|
||
</p>
|
||
</div>
|
||
|
||
{/* {paymentPreimage && (
|
||
<div className="w-full overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs mb-4">
|
||
<p className="text-xs text-center mb-1">Payment Preimage:</p>
|
||
<p className="break-all text-center">{paymentPreimage}</p>
|
||
</div>
|
||
)} */}
|
||
|
||
<div className="flex items-center text-green-500 mb-4">
|
||
<CheckCircledIcon className="mr-2 h-4 w-4" />
|
||
Zap sent successfully!
|
||
</div>
|
||
|
||
<Button variant="outline" onClick={() => {
|
||
setPaymentComplete(false);
|
||
setNwcPaymentStatus("");
|
||
setPaymentPreimage("");
|
||
}}>
|
||
Send Another Zap
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{(!paymentComplete || !isProcessing) && !paymentComplete && (
|
||
<div className="grid grid-cols-3 gap-2 mb-6">
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={() => handleZap(1)}
|
||
disabled={isProcessing}
|
||
>
|
||
1 sat
|
||
</Button>
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={() => handleZap(21)}
|
||
disabled={isProcessing}
|
||
>
|
||
21 sats
|
||
</Button>
|
||
<div className="flex">
|
||
<Input
|
||
className="mx-1"
|
||
placeholder="21"
|
||
value={customAmount}
|
||
onChange={(e) => setCustomAmount(e.target.value)}
|
||
disabled={isProcessing}
|
||
/>
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={handleCustomZap}
|
||
disabled={isProcessing}
|
||
>
|
||
{isProcessing ? <ReloadIcon className="h-4 w-4 animate-spin" /> : "send"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!paymentComplete && <hr className="my-4" />}
|
||
{!paymentComplete && <ZapButtonList events={events} />}
|
||
</div>
|
||
)}
|
||
|
||
{/* Original LNURL Payment Flow */}
|
||
{(!useNwc || invoice) && (
|
||
<>
|
||
{invoice ? (
|
||
<div className="flex flex-col items-center px-4 py-4">
|
||
{paymentComplete ? (
|
||
<div className="flex flex-col items-center justify-center mb-4 p-2 rounded-lg bg-green-50 dark:bg-green-900/20 w-[200px] h-[200px]">
|
||
<CheckCircledIcon className="h-24 w-24 text-green-500" />
|
||
<p className="text-lg font-semibold text-green-500 mt-4">
|
||
Payment Complete!
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="mb-4 p-2 bg-white rounded-lg">
|
||
<Link href={`lightning:${invoice}`} target="_blank" rel="noopener noreferrer">
|
||
<QRCode value={invoice} size={200} />
|
||
</Link>
|
||
</div>
|
||
)}
|
||
|
||
{!paymentComplete && (
|
||
<>
|
||
<p className="text-sm text-center mb-4">
|
||
Scan this QR code with a Lightning wallet to pay the invoice
|
||
</p>
|
||
<div className="w-full overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs mb-4">
|
||
<code className="break-all">{invoice}</code>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
|
||
{paymentComplete ? (
|
||
<div className="flex items-center text-green-500 mb-4">
|
||
<CheckCircledIcon className="mr-2 h-4 w-4" />
|
||
Zap sent successfully!
|
||
</div>
|
||
) : (
|
||
<Button
|
||
variant="outline"
|
||
className="mb-4"
|
||
onClick={() => checkPaymentStatus()}
|
||
disabled={isProcessing}
|
||
>
|
||
{isProcessing ? (
|
||
<>
|
||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Checking...
|
||
</>
|
||
) : (
|
||
"Check if paid"
|
||
)}
|
||
</Button>
|
||
)}
|
||
|
||
<Button variant="outline" onClick={() => setInvoice("")}>
|
||
{paymentComplete ? "Send Another Zap" : "Back to Zap Options"}
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="px-4 pt-4 grid grid-cols-3 gap-2">
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={() => handleZap(1)}
|
||
disabled={isProcessing || !lnurlPayInfo}
|
||
>
|
||
1 sat
|
||
</Button>
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={() => handleZap(21)}
|
||
disabled={isProcessing || !lnurlPayInfo}
|
||
>
|
||
21 sats
|
||
</Button>
|
||
<div className="flex">
|
||
<Input
|
||
className="mx-1"
|
||
placeholder="1000 sats"
|
||
value={customAmount}
|
||
onChange={(e) => setCustomAmount(e.target.value)}
|
||
disabled={isProcessing}
|
||
/>
|
||
<Button
|
||
variant={"outline"}
|
||
className="mx-1"
|
||
onClick={handleCustomZap}
|
||
disabled={isProcessing || !lnurlPayInfo}
|
||
>
|
||
{isProcessing ? <ReloadIcon className="h-4 w-4 animate-spin" /> : "send"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<hr className="my-4" />
|
||
<ZapButtonList events={events} />
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</DrawerContent>
|
||
</Drawer>
|
||
);
|
||
} |