ReceiveToOnchain.tsx raw
1 import {
2 ArrowLeftIcon,
3 CopyIcon,
4 ExternalLinkIcon,
5 HandCoinsIcon,
6 RefreshCwIcon,
7 } from "lucide-react";
8 import { useEffect, useRef, useState } from "react";
9 import { FixedFloatButton } from "src/components/FixedFloatButton";
10 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
11 import FormattedFiatAmount from "src/components/FormattedFiatAmount";
12 import Loading from "src/components/Loading";
13 import LottieLoading from "src/components/LottieLoading";
14 import LottieSuccess from "src/components/LottieSuccess";
15 import OnchainAddressDisplay from "src/components/OnchainAddressDisplay";
16 import QRCode from "src/components/QRCode";
17 import { Button } from "src/components/ui/button";
18 import {
19 Card,
20 CardContent,
21 CardFooter,
22 CardHeader,
23 CardTitle,
24 } from "src/components/ui/card";
25 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
26 import { LinkButton } from "src/components/ui/custom/link-button";
27 import { Separator } from "src/components/ui/separator";
28 import { useInfo } from "src/hooks/useInfo";
29 import { useMempoolApi } from "src/hooks/useMempoolApi";
30 import { useOnchainAddress } from "src/hooks/useOnchainAddress";
31 import { copyToClipboard } from "src/lib/clipboard";
32 import { MempoolUtxo } from "src/types";
33
34 export function ReceiveToOnchain() {
35 const { data: onchainAddress, getNewAddress } = useOnchainAddress();
36 const { data: mempoolAddressUtxos } = useMempoolApi<MempoolUtxo[]>(
37 onchainAddress ? `/address/${onchainAddress}/utxo` : undefined,
38 3000
39 );
40
41 const [txId, setTxId] = useState("");
42 const [confirmedAmountSat, setConfirmedAmountSat] = useState<number | null>(
43 null
44 );
45 const [pendingAmountSat, setPendingAmountSat] = useState<number | null>(null);
46 const startTimeRef = useRef(0);
47
48 useEffect(() => {
49 if (startTimeRef.current === 0) {
50 startTimeRef.current = Math.floor(Date.now() / 1000);
51 }
52 }, []);
53
54 const receiveAnother = async () => {
55 setTxId("");
56 setConfirmedAmountSat(null);
57 setPendingAmountSat(null);
58 startTimeRef.current = Math.floor(Date.now() / 1000);
59 await getNewAddress();
60 };
61
62 useEffect(() => {
63 if (
64 !mempoolAddressUtxos ||
65 mempoolAddressUtxos.length === 0 ||
66 startTimeRef.current === 0
67 ) {
68 return;
69 }
70
71 if (txId) {
72 const utxo = mempoolAddressUtxos.find((utxo) => utxo.txid === txId);
73 if (utxo?.status.confirmed) {
74 setConfirmedAmountSat(utxo.value);
75 setPendingAmountSat(null);
76 }
77 } else {
78 const unconfirmed = mempoolAddressUtxos.find(
79 (utxo) => !utxo.status.confirmed
80 );
81 if (unconfirmed) {
82 setTxId(unconfirmed.txid);
83 setPendingAmountSat(unconfirmed.value);
84 return;
85 }
86
87 const confirmed = mempoolAddressUtxos.find(
88 (utxo) =>
89 utxo.status.confirmed &&
90 !!utxo.status.block_time &&
91 utxo.status.block_time >= startTimeRef.current
92 );
93 if (confirmed) {
94 setTxId(confirmed.txid);
95 setConfirmedAmountSat(confirmed.value);
96 setPendingAmountSat(null);
97 }
98 }
99 }, [mempoolAddressUtxos, txId]);
100
101 if (!onchainAddress) {
102 return <Loading />;
103 }
104
105 return (
106 <>
107 {confirmedAmountSat ? (
108 <DepositSuccess
109 amountSat={confirmedAmountSat}
110 txId={txId}
111 onReceiveAnother={receiveAnother}
112 />
113 ) : txId ? (
114 <DepositPending amountSat={pendingAmountSat} txId={txId} />
115 ) : (
116 <Card>
117 <CardContent className="flex flex-col items-center gap-6">
118 <a
119 href={`bitcoin:${onchainAddress}`}
120 target="_blank"
121 className="flex justify-center"
122 >
123 <QRCode value={onchainAddress} paymentType="onchain" />
124 </a>
125 <div className="flex flex-wrap max-w-64 gap-2 items-center justify-center">
126 <OnchainAddressDisplay address={onchainAddress} />
127 </div>
128 </CardContent>
129 <CardFooter className="flex flex-col gap-3 pt-2">
130 <Button
131 className="w-full"
132 onClick={() => {
133 copyToClipboard(onchainAddress);
134 }}
135 variant="secondary"
136 >
137 <CopyIcon className="w-4 h-4" />
138 Copy Address
139 </Button>
140 <Button
141 className="w-full"
142 variant="outline"
143 onClick={getNewAddress}
144 >
145 <RefreshCwIcon className="h-4 w-4" />
146 New Address
147 </Button>
148 <Separator className="my-4" />
149 <FixedFloatButton
150 to="BTC"
151 address={onchainAddress}
152 className="w-full"
153 variant="outline"
154 >
155 <ExternalLinkIcon className="size-4" />
156 Top Up with Crypto
157 </FixedFloatButton>
158 </CardFooter>
159 </Card>
160 )}
161 </>
162 );
163 }
164
165 function DepositPending({
166 amountSat,
167 txId,
168 }: {
169 amountSat: number | null;
170 txId: string;
171 }) {
172 const { data: info } = useInfo();
173
174 return (
175 <Card className="w-full">
176 <CardHeader>
177 <CardTitle className="text-center">
178 Waiting for On-chain Confirmation...
179 </CardTitle>
180 </CardHeader>
181 <CardContent className="flex flex-col items-center gap-4">
182 <LottieLoading size={288} />
183 {amountSat && (
184 <div className="flex flex-col gap-1 items-center">
185 <p className="text-2xl font-medium slashed-zero">
186 <FormattedBitcoinAmount amountMsat={amountSat * 1000} />
187 </p>
188 <FormattedFiatAmount amountSat={amountSat} className="text-xl" />
189 </div>
190 )}
191 </CardContent>
192 <CardFooter className="flex flex-col gap-3 pt-2">
193 <ExternalLinkButton
194 to={`${info?.mempoolUrl}/tx/${txId}`}
195 variant="outline"
196 className="w-full"
197 >
198 <ExternalLinkIcon className="size-4" />
199 View on Mempool
200 </ExternalLinkButton>
201 </CardFooter>
202 </Card>
203 );
204 }
205
206 function DepositSuccess({
207 amountSat,
208 txId,
209 onReceiveAnother,
210 }: {
211 amountSat: number;
212 txId: string;
213 onReceiveAnother: () => void;
214 }) {
215 const { data: info } = useInfo();
216
217 return (
218 <Card className="w-full">
219 <CardHeader>
220 <CardTitle className="text-center">Transaction Received!</CardTitle>
221 </CardHeader>
222 <CardContent className="flex flex-col items-center gap-6">
223 <LottieSuccess />
224 <div className="flex flex-col gap-1 items-center">
225 <p className="text-2xl font-medium slashed-zero">
226 <FormattedBitcoinAmount amountMsat={amountSat * 1000} />
227 </p>
228 <FormattedFiatAmount amountSat={amountSat} className="text-xl" />
229 </div>
230 </CardContent>
231 <CardFooter className="flex flex-col gap-3 pt-2">
232 <ExternalLinkButton
233 to={`${info?.mempoolUrl}/tx/${txId}`}
234 variant="outline"
235 className="w-full"
236 >
237 <ExternalLinkIcon className="size-4" />
238 View on Mempool
239 </ExternalLinkButton>
240 <Button
241 type="button"
242 variant="outline"
243 className="w-full"
244 onClick={onReceiveAnother}
245 >
246 <HandCoinsIcon className="size-4" />
247 Receive Another Payment
248 </Button>
249 <LinkButton to="/wallet" variant="link" className="w-full">
250 <ArrowLeftIcon className="size-4" />
251 Back to Wallet
252 </LinkButton>
253 </CardFooter>
254 </Card>
255 );
256 }
257