import "./SendSats.css"; import React, { useEffect, useMemo, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { HexKey, RawEvent } from "@snort/nostr"; import { formatShort } from "Number"; import Icon from "Icons/Icon"; 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 { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "LNURL"; import { chunks, debounce } from "Util"; import { useWallet } from "Wallet"; import useLogin from "Hooks/useLogin"; import { generateRandomKey } from "Login"; import { EventPublisher } from "System/EventPublisher"; import messages from "./messages"; enum ZapType { PublicZap = 1, AnonZap = 2, PrivateZap = 3, NonZap = 4, } export interface SendSatsProps { onClose?: () => void; lnurl?: string; show?: boolean; invoice?: string; // shortcut to invoice qr tab title?: string; notice?: string; target?: string; note?: HexKey; author?: HexKey; } export default function SendSats(props: SendSatsProps) { const onClose = props.onClose || (() => undefined); const { note, author, target } = props; const login = useLogin(); const defaultZapAmount = login.preferences.defaultZapAmount; const amounts = [defaultZapAmount, 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 [handler, setHandler] = useState(); const [invoice, setInvoice] = useState(); const [amount, setAmount] = useState(defaultZapAmount); const [customAmount, setCustomAmount] = useState(); const [comment, setComment] = useState(); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [zapType, setZapType] = useState(ZapType.PublicZap); const [paying, setPaying] = useState(false); const { formatMessage } = useIntl(); const publisher = useEventPublisher(); const canComment = handler ? (handler.canZap && zapType !== ZapType.NonZap) || handler.maxCommentLength > 0 : false; const walletState = useWallet(); const wallet = walletState.wallet; useEffect(() => { if (props.show) { setError(undefined); setAmount(defaultZapAmount); setComment(undefined); setZapType(ZapType.PublicZap); setInvoice(props.invoice); setSuccess(undefined); } }, [props.show]); useEffect(() => { if (success && !success.url) { // Fire onClose when success is set with no URL action return debounce(1_000, () => { onClose(); }); } }, [success]); useEffect(() => { if (props.lnurl && props.show) { try { const h = new LNURL(props.lnurl); setHandler(h); h.load().catch(e => handleLNURLError(e, formatMessage(messages.InvoiceFail))); } catch (e) { if (e instanceof Error) { setError(e.message); } } } }, [props.lnurl, props.show]); const serviceAmounts = useMemo(() => { if (handler) { const min = handler.min / 1000; const max = handler.max / 1000; return amounts.filter(a => a >= min && a <= max); } return []; }, [handler]); const amountRows = useMemo(() => chunks(serviceAmounts, 3), [serviceAmounts]); const selectAmount = (a: number) => { setError(undefined); setAmount(a); }; async function loadInvoice() { if (!amount || !handler || !publisher) return null; let zap: RawEvent | undefined; if (author && zapType !== ZapType.NonZap) { const relays = Object.keys(login.relays.item); // use random key for anon zaps if (zapType === ZapType.AnonZap) { const randomKey = generateRandomKey(); console.debug("Generated new key for zap: ", randomKey); const publisher = new EventPublisher(randomKey.publicKey, randomKey.privateKey); zap = await publisher.zap(amount * 1000, author, relays, note, comment, eb => eb.tag(["anon", ""])); } else { zap = await publisher.zap(amount * 1000, author, relays, note, comment); } } try { const rsp = await handler.getInvoice(amount, comment, zap); if (rsp.pr) { setInvoice(rsp.pr); await payWithWallet(rsp); } } catch (e) { handleLNURLError(e, formatMessage(messages.InvoiceFail)); } } function handleLNURLError(e: unknown, fallback: string) { if (e instanceof LNURLError) { switch (e.code) { case LNURLErrorCode.ServiceUnavailable: { setError(formatMessage(messages.LNURLFail)); return; } case LNURLErrorCode.InvalidLNURL: { setError(formatMessage(messages.InvalidLNURL)); return; } } } setError(fallback); } function custom() { if (!handler) return null; const min = handler.min / 1000; const max = handler.max / 1000; return (
setCustomAmount(parseInt(e.target.value))} />
); } async function payWithWallet(invoice: LNURLInvoice) { try { if (wallet?.isReady) { setPaying(true); const res = await wallet.payInvoice(invoice?.pr ?? ""); console.log(res); setSuccess(invoice?.successAction ?? {}); } } catch (e: unknown) { console.warn(e); if (e instanceof Error) { setError(e.toString()); } } finally { setPaying(false); } } function renderAmounts(amount: number, amounts: number[]) { return (
{amounts.map(a => ( selectAmount(a)}> {emojis[a] && <>{emojis[a]} } {a === 1000 ? "1K" : formatShort(a)} ))}
); } function invoiceForm() { if (!handler || invoice) return null; return ( <>

{amountRows.map(amounts => renderAmounts(amount, amounts))} {custom()}
{canComment && ( setComment(e.target.value)} /> )}
{zapTypeSelector()} {(amount ?? 0) > 0 && ( )} ); } function zapTypeSelector() { if (!handler || !handler.canZap) return; const makeTab = (t: ZapType, n: React.ReactNode) => (
setZapType(t)}> {n}
); return ( <>

{makeTab(ZapType.PublicZap, )} {/*makeTab(ZapType.PrivateZap, "Private")*/} {makeTab(ZapType.AnonZap, )} {makeTab( ZapType.NonZap, )}
); } function payInvoice() { if (success || !invoice) return null; return ( <>
{props.notice && {props.notice}} {paying ? (

...

) : ( )}
{invoice && ( <>
)}
); } function successAction() { if (!success) return null; return (

{success?.description ?? }

{success.url && (

{success.url}

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

{props.title || title}

{invoiceForm()} {error &&

{error}

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