AutoChannel.tsx raw

   1  import { ChevronDownIcon } from "lucide-react";
   2  import React from "react";
   3  import { useNavigate } from "react-router";
   4  import { toast } from "sonner";
   5  import AppHeader from "src/components/AppHeader";
   6  import ExternalLink from "src/components/ExternalLink";
   7  import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
   8  import Loading from "src/components/Loading";
   9  import { Button } from "src/components/ui/button";
  10  import { Checkbox } from "src/components/ui/checkbox";
  11  import { LoadingButton } from "src/components/ui/custom/loading-button";
  12  import { Label } from "src/components/ui/label";
  13  import { Separator } from "src/components/ui/separator";
  14  import { useChannels } from "src/hooks/useChannels";
  15  
  16  import { useInfo } from "src/hooks/useInfo";
  17  import { AutoChannelRequest, AutoChannelResponse } from "src/types";
  18  import { request } from "src/utils/request";
  19  
  20  import { MempoolAlert } from "src/components/MempoolAlert";
  21  import { PayLightningInvoice } from "src/components/PayLightningInvoice";
  22  import { ChannelPublicPrivateAlert } from "src/components/channels/ChannelPublicPrivateAlert";
  23  
  24  import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg";
  25  import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg";
  26  import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
  27  import { LinkButton } from "src/components/ui/custom/link-button";
  28  
  29  export function AutoChannel() {
  30    const { data: info } = useInfo();
  31    const { data: channels } = useChannels(true);
  32    const [isLoading, setLoading] = React.useState(false);
  33    const [showAdvanced, setShowAdvanced] = React.useState(false);
  34    const [isPublic, setPublic] = React.useState(false);
  35  
  36    const navigate = useNavigate();
  37    const [invoice, setInvoice] = React.useState<string>();
  38    const [channelSizeSat, setChannelSizeSat] = React.useState<number>();
  39    const [, setPrevChannelIds] = React.useState<string[]>();
  40  
  41    React.useEffect(() => {
  42      if (channels) {
  43        setPrevChannelIds((current) => {
  44          if (current) {
  45            const newChannelId = channels.find(
  46              (channel) => !current?.includes(channel.id) && channel.fundingTxId
  47            )?.id;
  48  
  49            if (newChannelId) {
  50              console.info("Found new channel", newChannelId);
  51              navigate("/channels/auto/opening", {
  52                state: {
  53                  newChannelId,
  54                },
  55              });
  56            }
  57  
  58            return current;
  59          }
  60  
  61          return channels.map((channel) => channel.id);
  62        });
  63      }
  64    }, [channels, navigate]);
  65  
  66    if (!info || !channels) {
  67      return <Loading />;
  68    }
  69  
  70    async function openChannel() {
  71      if (!info || !channels) {
  72        return;
  73      }
  74      setLoading(true);
  75      try {
  76        const newInstantChannelInvoiceRequest: AutoChannelRequest = {
  77          isPublic,
  78        };
  79        const autoChannelResponse = await request<AutoChannelResponse>(
  80          "/api/alby/auto-channel",
  81          {
  82            method: "POST",
  83            headers: {
  84              "Content-Type": "application/json",
  85            },
  86            body: JSON.stringify(newInstantChannelInvoiceRequest),
  87          }
  88        );
  89        if (!autoChannelResponse) {
  90          throw new Error("unexpected auto channel response");
  91        }
  92  
  93        setInvoice(autoChannelResponse.invoice);
  94        setChannelSizeSat(autoChannelResponse.channelSizeSat);
  95      } catch (error) {
  96        setLoading(false);
  97        console.error(error);
  98        toast.error("Something went wrong. Please try again");
  99      }
 100    }
 101  
 102    return (
 103      <>
 104        <AppHeader
 105          pageTitle="Open a lightning channel"
 106          title="Open a lightning channel"
 107          description="Open a channel to another node on the lightning network"
 108        />
 109        <MempoolAlert />
 110        {invoice && channelSizeSat && (
 111          <div className="flex flex-col gap-4 items-center justify-center max-w-md">
 112            <p className="text-muted-foreground slashed-zero">
 113              Please pay the lightning invoice below which will cover the costs of
 114              opening your channel. You will receive a channel with{" "}
 115              <FormattedBitcoinAmount amountMsat={channelSizeSat * 1000} /> of
 116              receiving capacity.
 117            </p>
 118            <PayLightningInvoice invoice={invoice} />
 119  
 120            <Separator className="mt-8" />
 121            <p className="mt-8 text-sm mb-2 text-muted-foreground">
 122              Other options
 123            </p>
 124            <LinkButton
 125              to="/channels/outgoing"
 126              variant="secondary"
 127              className="w-full"
 128            >
 129              Open Channel with On-Chain Bitcoin
 130            </LinkButton>
 131            <ExternalLinkButton
 132              to="https://www.getalby.com/topup"
 133              variant="secondary"
 134              className="w-full"
 135            >
 136              Buy Bitcoin
 137            </ExternalLinkButton>
 138          </div>
 139        )}
 140        {!invoice && (
 141          <>
 142            <div className="flex flex-col gap-6 max-w-md text-muted-foreground">
 143              <img
 144                src={LightningNetworkDarkSVG}
 145                className="w-full hidden dark:block"
 146              />
 147              <img
 148                src={LightningNetworkLightSVG}
 149                className="w-full dark:hidden"
 150              />
 151  
 152              <>
 153                <p>
 154                  You're now going to open a new lightning channel that you can
 155                  use to send and receive payments using your Hub in the booming
 156                  bitcoin economy! To make things easy, Alby has picked a channel
 157                  partner for you from one of our recommended channel partners.
 158                </p>
 159                <p>
 160                  After paying a lightning invoice to cover on-chain fees, you'll
 161                  immediately be able to receive and send bitcoin through this
 162                  channel with your Hub.
 163                </p>
 164                <p className="text-muted-foreground">
 165                  Alby Hub works with selected service providers (LSPs) which
 166                  provide the best network connectivity and liquidity to receive
 167                  payments.{" "}
 168                  <ExternalLink
 169                    className="underline"
 170                    to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel"
 171                  >
 172                    Learn more
 173                  </ExternalLink>
 174                </p>
 175              </>
 176              {showAdvanced && (
 177                <>
 178                  <div className="mt-2 flex items-top space-x-2">
 179                    <Checkbox
 180                      id="public-channel"
 181                      onCheckedChange={() => setPublic(!isPublic)}
 182                      className="mr-2"
 183                    />
 184                    <div className="grid gap-1.5 leading-none">
 185                      <Label
 186                        htmlFor="public-channel"
 187                        className="cursor-pointer text-foreground"
 188                      >
 189                        Public Channel
 190                      </Label>
 191                      <p className="text-xs text-muted-foreground">
 192                        Not recommended for most users.{" "}
 193                        <ExternalLink
 194                          className="underline"
 195                          to="https://guides.getalby.com/user-guide/alby-hub/faq/should-i-open-a-private-or-public-channel"
 196                        >
 197                          Learn more
 198                        </ExternalLink>
 199                      </p>
 200                    </div>
 201                  </div>
 202                </>
 203              )}
 204              {!showAdvanced && (
 205                <div>
 206                  <Button
 207                    type="button"
 208                    variant="link"
 209                    className="text-muted-foreground text-xs px-0"
 210                    onClick={() => setShowAdvanced((current) => !current)}
 211                  >
 212                    Advanced Options
 213                    <ChevronDownIcon className="size-4 ml-1" />
 214                  </Button>
 215                </div>
 216              )}
 217              {channels?.some((channel) => channel.public !== !!isPublic) && (
 218                <ChannelPublicPrivateAlert />
 219              )}
 220              <LoadingButton loading={isLoading} onClick={openChannel}>
 221                Open Channel
 222              </LoadingButton>
 223            </div>
 224          </>
 225        )}
 226      </>
 227    );
 228  }
 229