Send.tsx raw
1 import { Invoice } from "@getalby/lightning-tools/bolt11";
2 import { LightningAddress } from "@getalby/lightning-tools/lnurl";
3 import { validate as validateBitcoinAddress } from "bitcoin-address-validation";
4 import { ClipboardPasteIcon } from "lucide-react";
5 import React from "react";
6 import { useNavigate, useSearchParams } from "react-router";
7 import { toast } from "sonner";
8 import AppHeader from "src/components/AppHeader";
9 import { CryptoSwapAlert } from "src/components/CryptoSwapAlert";
10 import Loading from "src/components/Loading";
11 import { Button } from "src/components/ui/button";
12 import { LoadingButton } from "src/components/ui/custom/loading-button";
13 import { Input } from "src/components/ui/input";
14 import { Label } from "src/components/ui/label";
15 import { useBalances } from "src/hooks/useBalances";
16 import { useChannels } from "src/hooks/useChannels";
17 import { parseBip21 } from "src/utils/parseBip21";
18
19 export default function Send() {
20 const { data: balances } = useBalances();
21 const { data: channels } = useChannels();
22 const navigate = useNavigate();
23 const [searchParams] = useSearchParams();
24
25 const [recipient, setRecipient] = React.useState("");
26 const [isLoading, setLoading] = React.useState(false);
27 const [showSwapAlert, setShowSwapAlert] = React.useState(false);
28
29 const handleBip21 = React.useCallback(
30 (uri: string) => {
31 const bip21 = parseBip21(uri);
32 if (bip21.lightning) {
33 const invoice = new Invoice({ pr: bip21.lightning });
34 if (invoice.satoshi === 0) {
35 navigate(`/wallet/send/0-amount`, {
36 state: { args: { paymentRequest: invoice } },
37 });
38 } else {
39 navigate(`/wallet/send/confirm-payment`, {
40 state: { args: { paymentRequest: invoice } },
41 });
42 }
43 return;
44 }
45 if (!bip21.address || !validateBitcoinAddress(bip21.address)) {
46 throw new Error("invalid bitcoin address");
47 }
48 navigate(`/wallet/send/onchain`, {
49 state: {
50 args: {
51 address: bip21.address,
52 amountSat: bip21.amountSat ? String(bip21.amountSat) : undefined,
53 },
54 },
55 });
56 },
57 [navigate]
58 );
59
60 React.useEffect(() => {
61 const uri = searchParams.get("bip21");
62 if (!uri) {
63 return;
64 }
65 try {
66 handleBip21(uri);
67 } catch (error) {
68 toast.error("Invalid Bitcoin URI", { description: "" + error });
69 }
70 }, [searchParams, handleBip21]);
71
72 const paste = async () => {
73 const text = await navigator.clipboard.readText();
74 setRecipient(text.trim());
75 };
76
77 const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
78 event.preventDefault();
79 try {
80 setLoading(true);
81 if (/^bitcoin:/i.test(recipient)) {
82 handleBip21(recipient);
83 return;
84 }
85
86 if (validateBitcoinAddress(recipient)) {
87 navigate(`/wallet/send/onchain`, {
88 state: {
89 args: { address: recipient },
90 },
91 });
92 return;
93 }
94
95 if (recipient.includes("@")) {
96 const lnAddress = new LightningAddress(recipient);
97 await lnAddress.fetch();
98 if (lnAddress.lnurlpData) {
99 navigate(`/wallet/send/lnurl-pay`, {
100 state: {
101 args: { lnAddress },
102 },
103 });
104 return;
105 }
106 }
107
108 const invoice = new Invoice({ pr: recipient });
109 if (invoice.satoshi === 0) {
110 navigate(`/wallet/send/0-amount`, {
111 state: {
112 args: { paymentRequest: invoice },
113 },
114 });
115 return;
116 }
117
118 navigate(`/wallet/send/confirm-payment`, {
119 state: {
120 args: { paymentRequest: invoice },
121 },
122 });
123 } catch (error) {
124 setShowSwapAlert(true);
125 toast.error("Invalid recipient", {
126 description: "" + error,
127 });
128 console.error(error);
129 } finally {
130 setLoading(false);
131 }
132 };
133
134 if (!balances || !channels) {
135 return <Loading />;
136 }
137
138 return (
139 <div className="grid gap-4">
140 <AppHeader pageTitle="Send" title="Send" />
141 <div className="w-full md:max-w-lg">
142 <form onSubmit={onSubmit} className="grid gap-6">
143 {showSwapAlert && <CryptoSwapAlert />}
144 <div className="grid gap-2">
145 <Label htmlFor="recipient">Recipient</Label>
146 <div className="flex gap-2">
147 <Input
148 id="recipient"
149 type="text"
150 value={recipient}
151 autoFocus
152 placeholder="Invoice, lightning address, on-chain address"
153 onChange={(e) => {
154 setRecipient(e.target.value.trim());
155 setShowSwapAlert(false);
156 }}
157 />
158 <Button
159 type="button"
160 variant="outline"
161 className="px-2"
162 onClick={paste}
163 >
164 <ClipboardPasteIcon className="w-4 h-4" />
165 </Button>
166 </div>
167 </div>
168 <LoadingButton
169 loading={isLoading}
170 type="submit"
171 disabled={!recipient}
172 className="flex-1"
173 >
174 Continue
175 </LoadingButton>
176 </form>
177 </div>
178 </div>
179 );
180 }
181