LnurlPay.tsx raw
1 import { Invoice } from "@getalby/lightning-tools";
2 import type { LightningAddress } from "@getalby/lightning-tools/lnurl";
3 import { XIcon } from "lucide-react";
4 import React from "react";
5 import { Link, useLocation, useNavigate } from "react-router";
6 import { toast } from "sonner";
7 import AppHeader from "src/components/AppHeader";
8 import { CurrencyInputField } from "src/components/CurrencyInputField";
9 import { InsufficientLightningBalanceAlert } from "src/components/InsufficientLightningBalanceAlert";
10 import Loading from "src/components/Loading";
11 import { PaymentFailedAlert } from "src/components/PaymentFailedAlert";
12 import { PendingPaymentAlert } from "src/components/PendingPaymentAlert";
13 import { LinkButton } from "src/components/ui/custom/link-button";
14 import { LoadingButton } from "src/components/ui/custom/loading-button";
15 import { Input } from "src/components/ui/input";
16 import { Label } from "src/components/ui/label";
17 import { useBalances } from "src/hooks/useBalances";
18 import PayFromSelect from "src/screens/wallet/send/PayFromSelect";
19 import {
20 PayInvoiceRequest,
21 PayInvoiceResponse,
22 TransactionMetadata,
23 } from "src/types";
24 import { request } from "src/utils/request";
25
26 export default function LnurlPay() {
27 const { state } = useLocation();
28 const navigate = useNavigate();
29 const { data: balances } = useBalances();
30
31 const lnAddress = state?.args?.lnAddress as LightningAddress;
32 const identifier = lnAddress.lnurlpData?.identifier;
33 const [appId, setAppId] = React.useState<number>();
34 const [amountSat, setAmountSat] = React.useState("");
35 const [comment, setComment] = React.useState("");
36 const [isLoading, setLoading] = React.useState(false);
37 const [invoice, setInvoice] = React.useState<Invoice>();
38 const [errorMessage, setErrorMessage] = React.useState("");
39
40 const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
41 event.preventDefault();
42 setErrorMessage("");
43 try {
44 if (!lnAddress) {
45 throw new Error("no lightning address set");
46 }
47 setLoading(true);
48 const invoice = await lnAddress.requestInvoice({
49 satoshi: parseInt(amountSat),
50 comment,
51 });
52 setInvoice(invoice);
53 const metadata: TransactionMetadata = {
54 ...(comment && { comment }),
55 ...(identifier && { recipient_data: { identifier } }),
56 };
57 const payload: PayInvoiceRequest = {
58 metadata,
59 fromAppId: appId,
60 };
61 const payInvoiceResponse = await request<PayInvoiceResponse>(
62 `/api/payments/${invoice.paymentRequest}`,
63 {
64 method: "POST",
65 body: JSON.stringify(payload),
66 headers: {
67 "Content-Type": "application/json",
68 },
69 }
70 );
71 if (!payInvoiceResponse?.preimage) {
72 throw new Error("No preimage in response");
73 }
74 navigate(`/wallet/send/success`, {
75 state: {
76 preimage: payInvoiceResponse.preimage,
77 invoice,
78 to: lnAddress.address,
79 pageTitle: "Send to Lightning Address",
80 },
81 replace: true,
82 });
83 toast("Successfully paid invoice");
84 } catch (e) {
85 console.error(e);
86 setErrorMessage("" + e);
87 toast.error("Failed to send payment", {
88 description: "" + e,
89 });
90 } finally {
91 setLoading(false);
92 }
93 };
94
95 React.useEffect(() => {
96 if (!lnAddress) {
97 navigate("/wallet/send");
98 }
99 }, [navigate, lnAddress]);
100
101 if (!balances || !lnAddress) {
102 return <Loading />;
103 }
104
105 return (
106 <div className="grid gap-4">
107 <AppHeader
108 pageTitle="Send to Lightning Address"
109 title="Send to Lightning Address"
110 />
111 <div className="md:max-w-lg grid gap-4">
112 <PendingPaymentAlert />
113 {errorMessage && invoice && (
114 <PaymentFailedAlert
115 errorMessage={errorMessage}
116 invoice={invoice.paymentRequest}
117 />
118 )}
119 <form onSubmit={onSubmit} className="grid gap-6">
120 <div className="grid gap-2">
121 <div className="text-sm font-medium">Recipient</div>
122 <div className="flex items-center justify-between gap-2">
123 <p className="text-sm break-all">{lnAddress.address}</p>
124 <Link to="/wallet/send">
125 <XIcon className="w-4 h-4 cursor-pointer text-muted-foreground" />
126 </Link>
127 </div>
128 </div>
129 {lnAddress.lnurlpData?.description && (
130 <div className="grid gap-2">
131 <Label>Description</Label>
132 <p className="text-muted-foreground text-sm">
133 {lnAddress.lnurlpData.description}
134 </p>
135 </div>
136 )}
137 <CurrencyInputField
138 id="amount"
139 valueSat={amountSat}
140 onValueSatChange={setAmountSat}
141 minSat={1}
142 maxSat={balances.lightning.totalSpendableSat}
143 required
144 autoFocus
145 contextRows={[
146 {
147 label: "Lightning balance",
148 amountSat: balances.lightning.totalSpendableSat,
149 },
150 ]}
151 />
152 {!!lnAddress.lnurlpData?.commentAllowed && (
153 <div className="grid gap-2">
154 <Label htmlFor="comment">Comment</Label>
155 <Input
156 id="comment"
157 type="text"
158 value={comment}
159 placeholder="Optional"
160 onChange={(e) => {
161 setComment(e.target.value);
162 }}
163 />
164 </div>
165 )}
166 <PayFromSelect appId={appId} onChange={setAppId} />
167 <InsufficientLightningBalanceAlert amountSat={+amountSat} />
168 <div className="flex gap-2">
169 <LinkButton to="/wallet/send" variant="outline">
170 Back
171 </LinkButton>
172 <LoadingButton loading={isLoading} type="submit" className="flex-1">
173 Send
174 </LoadingButton>
175 </div>
176 </form>
177 </div>
178 </div>
179 );
180 }
181