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