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