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