181 lines
4.6 KiB
TypeScript
181 lines
4.6 KiB
TypeScript
import "./send-zap.css";
|
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
import { useEffect, useState } from "react";
|
|
import { LNURL } from "@snort/shared";
|
|
import { NostrEvent, EventPublisher } from "@snort/system";
|
|
import { formatSats } from "../number";
|
|
import { Icon } from "./icon";
|
|
import AsyncButton from "./async-button";
|
|
import { findTag } from "utils";
|
|
import { Relays } from "index";
|
|
import QrCode from "./qr-code";
|
|
|
|
interface SendZapsProps {
|
|
lnurl: string;
|
|
ev?: NostrEvent;
|
|
targetName?: string;
|
|
onFinish: () => void;
|
|
}
|
|
|
|
function SendZaps({ lnurl, ev, targetName, onFinish }: SendZapsProps) {
|
|
const UsdRate = 30_000;
|
|
|
|
const satsAmounts = [
|
|
100, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000,
|
|
];
|
|
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
|
|
const [isFiat, setIsFiat] = useState(false);
|
|
const [svc, setSvc] = useState<LNURL>();
|
|
const [amount, setAmount] = useState(satsAmounts[0]);
|
|
const [comment, setComment] = useState("");
|
|
const [invoice, setInvoice] = useState("");
|
|
|
|
const name = targetName ?? svc?.name;
|
|
async function loadService() {
|
|
const s = new LNURL(lnurl);
|
|
await s.load();
|
|
setSvc(s);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!svc) {
|
|
loadService().catch(console.warn);
|
|
}
|
|
}, [lnurl]);
|
|
|
|
async function send() {
|
|
if (!svc) return;
|
|
const pub = await EventPublisher.nip7();
|
|
if (!pub) return;
|
|
|
|
const amountInSats = isFiat ? Math.floor((amount / UsdRate) * 1e8) : amount;
|
|
let zap: NostrEvent | undefined;
|
|
if (ev) {
|
|
zap = await pub.zap(
|
|
amountInSats * 1000,
|
|
ev.pubkey,
|
|
Relays,
|
|
undefined,
|
|
comment,
|
|
(eb) => {
|
|
return eb.tag(["a", `${ev.kind}:${ev.pubkey}:${findTag(ev, "d")}`]);
|
|
}
|
|
);
|
|
}
|
|
const invoice = await svc.getInvoice(amountInSats, comment, zap);
|
|
if (!invoice.pr) return;
|
|
|
|
if (window.webln) {
|
|
await window.webln.enable();
|
|
await window.webln.sendPayment(invoice.pr);
|
|
onFinish();
|
|
} else {
|
|
setInvoice(invoice.pr);
|
|
}
|
|
}
|
|
|
|
function input() {
|
|
if (invoice) return;
|
|
return (
|
|
<>
|
|
<div className="flex g12">
|
|
<span
|
|
className={`pill${isFiat ? "" : " active"}`}
|
|
onClick={() => {
|
|
setIsFiat(false);
|
|
setAmount(satsAmounts[0]);
|
|
}}
|
|
>
|
|
SATS
|
|
</span>
|
|
<span
|
|
className={`pill${isFiat ? " active" : ""}`}
|
|
onClick={() => {
|
|
setIsFiat(true);
|
|
setAmount(usdAmounts[0]);
|
|
}}
|
|
>
|
|
USD
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<small>Zap amount in {isFiat ? "USD" : "sats"}</small>
|
|
<div className="amounts">
|
|
{(isFiat ? usdAmounts : satsAmounts).map((a) => (
|
|
<span
|
|
key={a}
|
|
className={`pill${a === amount ? " active" : ""}`}
|
|
onClick={() => setAmount(a)}
|
|
>
|
|
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<small>Your comment for {name}</small>
|
|
<div className="input">
|
|
<textarea
|
|
placeholder="Nice!"
|
|
value={comment}
|
|
onChange={(e) => setComment(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<AsyncButton onClick={send} className="btn btn-primary">
|
|
Zap!
|
|
</AsyncButton>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function payInvoice() {
|
|
if (!invoice) return;
|
|
|
|
const link = `lightning:${invoice}`;
|
|
return <QrCode data={link} link={link} />;
|
|
}
|
|
|
|
return (
|
|
<div className="send-zap">
|
|
<h3>
|
|
Zap {name}
|
|
<Icon name="zap" />
|
|
</h3>
|
|
{input()}
|
|
{payInvoice()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SendZapsDialog({
|
|
lnurl,
|
|
ev,
|
|
targetName,
|
|
}: Omit<SendZapsProps, "onFinish">) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
return (
|
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
<Dialog.Trigger asChild>
|
|
<button className="btn btn-primary zap">
|
|
<span className="hide-on-mobile">Zap</span>
|
|
<Icon name="zap" size={16} />
|
|
</button>
|
|
</Dialog.Trigger>
|
|
<Dialog.Portal>
|
|
<Dialog.Overlay className="dialog-overlay" />
|
|
<Dialog.Content className="dialog-content">
|
|
<SendZaps
|
|
lnurl={lnurl}
|
|
ev={ev}
|
|
targetName={targetName}
|
|
onFinish={() => setIsOpen(false)}
|
|
/>
|
|
</Dialog.Content>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|
|
);
|
|
}
|