Files
snort/src/Element/LNURLTip.tsx
2023-02-05 23:44:31 +01:00

256 lines
8.8 KiB
TypeScript

import "./LNURLTip.css";
import { useEffect, useMemo, useState } from "react";
import { bech32ToText } from "Util";
import { HexKey } from "Nostr";
import useEventPublisher from "Feed/EventPublisher";
import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import useWebln from "Hooks/useWebln";
interface LNURLService {
nostrPubkey?: HexKey
minSendable?: number,
maxSendable?: number,
metadata: string,
callback: string,
commentAllowed?: number
}
interface LNURLInvoice {
pr: string,
successAction?: LNURLSuccessAction
}
interface LNURLSuccessAction {
description?: string,
url?: string
}
export interface LNURLTipProps {
onClose?: () => void,
svc?: string,
show?: boolean,
invoice?: string, // shortcut to invoice qr tab
title?: string,
notice?: string
note?: HexKey
author?: HexKey
}
export default function LNURLTip(props: LNURLTipProps) {
const onClose = props.onClose || (() => { });
const service = props.svc;
const show = props.show || false;
const { note, author } = props
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
const [payService, setPayService] = useState<LNURLService>();
const [amount, setAmount] = useState<number>();
const [customAmount, setCustomAmount] = useState<number>();
const [invoice, setInvoice] = useState<LNURLInvoice>();
const [comment, setComment] = useState<string>();
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<LNURLSuccessAction>();
const webln = useWebln(show);
const publisher = useEventPublisher();
useEffect(() => {
if (show && !props.invoice) {
loadService()
.then(a => setPayService(a!))
.catch(() => setError("Failed to load LNURL service"));
} else {
setPayService(undefined);
setError(undefined);
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
setAmount(undefined);
setComment(undefined);
setSuccess(undefined);
}
}, [show, service]);
const serviceAmounts = useMemo(() => {
if (payService) {
let min = (payService.minSendable ?? 0) / 1000;
let max = (payService.maxSendable ?? 0) / 1000;
return amounts.filter(a => a >= min && a <= max);
}
return [];
}, [payService]);
const metadata = useMemo(() => {
if (payService) {
let meta: string[][] = JSON.parse(payService.metadata);
let desc = meta.find(a => a[0] === "text/plain");
let image = meta.find(a => a[0] === "image/png;base64");
return {
description: desc ? desc[1] : null,
image: image ? image[1] : null
};
}
return null;
}, [payService]);
const selectAmount = (a: number) => {
setError(undefined);
setInvoice(undefined);
setAmount(a);
};
async function fetchJson<T>(url: string) {
let rsp = await fetch(url);
if (rsp.ok) {
let data: T = await rsp.json();
console.log(data);
setError(undefined);
return data;
}
return null;
}
async function loadService(): Promise<LNURLService | null> {
if (service) {
let isServiceUrl = service.toLowerCase().startsWith("lnurl");
if (isServiceUrl) {
let serviceUrl = bech32ToText(service);
return await fetchJson(serviceUrl);
} else {
let ns = service.split("@");
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
}
}
return null;
}
async function loadInvoice() {
if (!amount || !payService) return null;
let url = ''
const amountParam = `amount=${Math.floor(amount * 1000)}`
const commentParam = comment ? `&comment=${encodeURIComponent(comment)}` : ""
if (payService.nostrPubkey && author) {
const ev = await publisher.zap(author, note, comment)
const nostrParam = ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
} else {
url = `${payService.callback}?${amountParam}${commentParam}`;
}
try {
let rsp = await fetch(url);
if (rsp.ok) {
let data = await rsp.json();
console.log(data);
if (data.status === "ERROR") {
setError(data.reason);
} else {
setInvoice(data);
setError("");
payWebLNIfEnabled(data);
}
} else {
setError("Failed to load invoice");
}
} catch (e) {
setError("Failed to load invoice");
}
};
function custom() {
let min = (payService?.minSendable ?? 0) / 1000;
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
return (
<div className="flex mb10">
<input type="number" min={min} max={max} className="f-grow mr10" value={customAmount} onChange={(e) => setCustomAmount(parseInt(e.target.value))} />
<div className="btn" onClick={() => selectAmount(customAmount!)}>Confirm</div>
</div>
);
}
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
try {
if (webln?.enabled) {
let res = await webln.sendPayment(invoice!.pr);
console.log(res);
setSuccess(invoice!.successAction || {});
}
} catch (e: any) {
setError(e.toString());
console.warn(e);
}
}
function invoiceForm() {
if (invoice) return null;
return (
<>
<div className="f-ellipsis mb10">{metadata?.description ?? service}</div>
<div className="flex">
{(payService?.commentAllowed ?? 0) > 0 ?
<input type="text" placeholder="Comment" className="mb10 f-grow" maxLength={payService?.commentAllowed} onChange={(e) => setComment(e.target.value)} /> : null}
</div>
<div className="mb10">
{serviceAmounts.map(a => <span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
{a.toLocaleString()}
</span>)}
{payService ?
<span className={`sat-amount ${amount === -1 ? "active" : ""}`} onClick={() => selectAmount(-1)}>
Custom
</span> : null}
</div>
{amount === -1 ? custom() : null}
{(amount ?? 0) > 0 && <button type="button" className="mb10" onClick={() => loadInvoice()}>Get Invoice</button>}
</>
)
}
function payInvoice() {
if (success) return null;
const pr = invoice?.pr;
return (
<>
<div className="invoice">
{props.notice && <b className="error">{props.notice}</b>}
<QrCode data={pr} link={`lightning:${pr}`} />
<div className="actions">
{pr && (
<>
<div className="copy-action">
<Copy text={pr} maxSize={26} />
</div>
<div className="pay-actions">
<button type="button" onClick={() => window.open(`lightning:${pr}`)}>
Open Wallet
</button>
</div>
</>
)}
</div>
</div>
</>
)
}
function successAction() {
if (!success) return null;
return (
<>
<p>{success?.description ?? "Paid!"}</p>
{success.url ? <a href={success.url} rel="noreferrer" target="_blank">{success.url}</a> : null}
</>
)
}
const defaultTitle = payService?.nostrPubkey ? "⚡️ Send Zap!" : "⚡️ Send sats";
if (!show) return null;
return (
<Modal onClose={onClose}>
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
<h2>{props.title || defaultTitle}</h2>
{invoiceForm()}
{error ? <p className="error">{error}</p> : null}
{payInvoice()}
{successAction()}
</div>
</Modal>
)
}