import "./SendSats.css"; import { useEffect, useMemo, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { formatShort } from "Number"; import { bech32ToText } from "Util"; import { HexKey } from "Nostr"; import Check from "Icons/Check"; import Zap from "Icons/Zap"; import Close from "Icons/Close"; import useEventPublisher from "Feed/EventPublisher"; import ProfileImage from "Element/ProfileImage"; import Modal from "Element/Modal"; import QrCode from "Element/QrCode"; import Copy from "Element/Copy"; import useWebln from "Hooks/useWebln"; import useHorizontalScroll from "Hooks/useHorizontalScroll"; import messages from "./messages"; 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; target?: 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, target } = props; const amounts = [ 500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000, ]; const emojis: Record = { 1_000: "👍", 5_000: "💜", 10_000: "😍", 20_000: "🤩", 50_000: "🔥", 100_000: "🚀", 1_000_000: "🤯", }; const [payService, setPayService] = useState(); const [amount, setAmount] = useState(500); const [customAmount, setCustomAmount] = useState(); const [invoice, setInvoice] = useState(); const [comment, setComment] = useState(); const [error, setError] = useState(); const [success, setSuccess] = useState(); const webln = useWebln(show); const { formatMessage } = useIntl(); const publisher = useEventPublisher(); const horizontalScroll = useHorizontalScroll(); const canComment = (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey; useEffect(() => { if (show && !props.invoice) { loadService() .then((a) => setPayService(a!)) .catch(() => setError(formatMessage(messages.LNURLFail))); } else { setPayService(undefined); setError(undefined); setInvoice(props.invoice ? { pr: props.invoice } : undefined); setAmount(500); 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(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 { 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 && payService?.commentAllowed ? `&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(formatMessage(messages.InvoiceFail)); } } catch (e) { setError(formatMessage(messages.InvoiceFail)); } } function custom() { let min = (payService?.minSendable ?? 1000) / 1000; let max = (payService?.maxSendable ?? 21_000_000_000) / 1000; return (
setCustomAmount(parseInt(e.target.value))} />
); } 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 ( <>

{serviceAmounts.map((a) => ( selectAmount(a)} > {emojis[a] && <>{emojis[a]} } {formatShort(a)} ))}
{payService && custom()}
{canComment && ( setComment(e.target.value)} /> )}
{(amount ?? 0) > 0 && ( )} ); } function payInvoice() { if (success) return null; const pr = invoice?.pr; return ( <>
{props.notice && {props.notice}}
{pr && ( <>
)}
); } function successAction() { if (!success) return null; return (

{success?.description ?? }

{success.url && (

{success.url}

)}
); } const defaultTitle = payService?.nostrPubkey ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats); const title = target ? formatMessage(messages.ToTarget, { action: defaultTitle, target, }) : defaultTitle; if (!show) return null; return (
e.stopPropagation()}>
{author && }

{props.title || title}

{invoiceForm()} {error &&

{error}

} {payInvoice()} {successAction()}
); }