FirstChannel.tsx raw

   1  import { ChevronDownIcon, InfoIcon } 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 {
  18    AutoChannelRequest,
  19    AutoChannelResponse,
  20    LSPChannelOfferPaymentMethod,
  21  } from "src/types";
  22  import { request } from "src/utils/request";
  23  
  24  import { Invoice } from "@getalby/lightning-tools";
  25  import { MempoolAlert } from "src/components/MempoolAlert";
  26  import { PayLightningInvoice } from "src/components/PayLightningInvoice";
  27  import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
  28  
  29  import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg";
  30  import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg";
  31  import { LSPTermsDialog } from "src/components/channels/LSPTermsDialog";
  32  import FormattedFiatAmount from "src/components/FormattedFiatAmount";
  33  import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
  34  import { LinkButton } from "src/components/ui/custom/link-button";
  35  import {
  36    Field,
  37    FieldContent,
  38    FieldLabel,
  39    FieldTitle,
  40  } from "src/components/ui/field";
  41  import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group";
  42  import {
  43    Tooltip,
  44    TooltipContent,
  45    TooltipProvider,
  46    TooltipTrigger,
  47  } from "src/components/ui/tooltip";
  48  import { useLSPChannelOffer } from "src/hooks/useLSPChannelOffer";
  49  import { cn } from "src/lib/utils";
  50  import { openLink } from "src/utils/openLink";
  51  
  52  export function FirstChannel() {
  53    const { data: info } = useInfo();
  54    const { data: channels } = useChannels(true);
  55    const [isLoading, setLoading] = React.useState(false);
  56    const [showAdvanced, setShowAdvanced] = React.useState(false);
  57    const [isPublic, setPublic] = React.useState(false);
  58    const { data: lspChannelOffer } = useLSPChannelOffer();
  59  
  60    const navigate = useNavigate();
  61    const [invoice, setInvoice] = React.useState<string>();
  62    const [channelSizeSat, setChannelSizeSat] = React.useState<number>();
  63    const [currentPaymentMethod, setCurrentPaymentMethod] =
  64      React.useState<LSPChannelOfferPaymentMethod>();
  65  
  66    React.useEffect(() => {
  67      if (channels?.length) {
  68        navigate("/channels/first/opening");
  69      }
  70    }, [channels, navigate]);
  71  
  72    React.useEffect(() => {
  73      if (info && !info.albyAccountConnected) {
  74        navigate("/channels/incoming");
  75      }
  76    }, [info, navigate]);
  77  
  78    if (!info?.albyAccountConnected || !channels || !lspChannelOffer) {
  79      return <Loading />;
  80    }
  81  
  82    async function openChannel() {
  83      if (!info || !channels || !lspChannelOffer) {
  84        return;
  85      }
  86      if (
  87        (lspChannelOffer.currentPaymentMethod === "card" ||
  88          lspChannelOffer.currentPaymentMethod === "wallet") &&
  89        currentPaymentMethod !== lspChannelOffer.currentPaymentMethod
  90      ) {
  91        toast.error("Payment method incorrectly configured", {
  92          description: currentPaymentMethod
  93            ? "Please switch the payment method and confirm the change in your Alby Account settings"
  94            : "Please choose a payment method",
  95        });
  96        return;
  97      }
  98      setLoading(true);
  99      try {
 100        const newInstantChannelInvoiceRequest: AutoChannelRequest = {
 101          isPublic,
 102        };
 103        const autoChannelResponse = await request<AutoChannelResponse>(
 104          "/api/alby/auto-channel",
 105          {
 106            method: "POST",
 107            headers: {
 108              "Content-Type": "application/json",
 109            },
 110            body: JSON.stringify(newInstantChannelInvoiceRequest),
 111          }
 112        );
 113        if (!autoChannelResponse) {
 114          throw new Error("unexpected auto channel response");
 115        }
 116  
 117        setInvoice(autoChannelResponse.invoice);
 118        setChannelSizeSat(autoChannelResponse.channelSizeSat);
 119      } catch (error) {
 120        setLoading(false);
 121        console.error(error);
 122        toast.error("Something went wrong. Please try again");
 123      }
 124    }
 125  
 126    return (
 127      <>
 128        <AppHeader
 129          pageTitle="Open Your First Channel"
 130          title="Open Your First Channel"
 131          description="Open a channel to another lightning network node to join the lightning network"
 132        />
 133        <MempoolAlert />
 134        {invoice && channelSizeSat && (
 135          <div className="flex flex-col gap-4 items-center justify-center max-w-md">
 136            <p className="text-muted-foreground slashed-zero">
 137              Alby Hub works with selected service providers (LSPs) which provide
 138              the best network connectivity and liquidity to receive payments. To
 139              quickly get started you can buy a channel from an LSP by paying the
 140              lightning invoice below.
 141            </p>
 142            <div className="border rounded-lg slashed-zero w-full">
 143              <Table>
 144                <TableBody>
 145                  <TableRow>
 146                    <TableCell className="font-medium p-3">
 147                      Incoming Liquidity
 148                    </TableCell>
 149                    <TableCell className="text-right p-3">
 150                      <FormattedBitcoinAmount
 151                        amountMsat={channelSizeSat * 1000}
 152                      />
 153                    </TableCell>
 154                  </TableRow>
 155                  {invoice && (
 156                    <TableRow>
 157                      <TableCell className="font-medium p-3">
 158                        Amount to pay
 159                      </TableCell>
 160                      <TableCell className="font-semibold text-right p-3">
 161                        <FormattedBitcoinAmount
 162                          amountMsat={new Invoice({ pr: invoice }).satoshi * 1000}
 163                        />
 164                      </TableCell>
 165                    </TableRow>
 166                  )}
 167                </TableBody>
 168              </Table>
 169            </div>
 170            <PayLightningInvoice invoice={invoice} />
 171  
 172            <Separator className="mt-2" />
 173            <p className="mt-8 text-sm mb-2 text-muted-foreground">
 174              Other options
 175            </p>
 176            <LinkButton
 177              to="/channels/outgoing"
 178              variant="secondary"
 179              className="w-full"
 180            >
 181              Open Channel with On-Chain Bitcoin
 182            </LinkButton>
 183            <ExternalLinkButton
 184              to="https://www.getalby.com/topup"
 185              variant="secondary"
 186              className="w-full"
 187            >
 188              Buy Bitcoin
 189            </ExternalLinkButton>
 190          </div>
 191        )}
 192        {!invoice && (
 193          <>
 194            <div className="flex flex-col gap-6 max-w-md text-muted-foreground">
 195              <img
 196                src={LightningNetworkDarkSVG}
 197                className="w-full hidden dark:block"
 198              />
 199              <img
 200                src={LightningNetworkLightSVG}
 201                className="w-full dark:hidden"
 202              />
 203              <p>
 204                You're now going to open your first lightning channel and can
 205                begin using your Hub in the booming bitcoin economy!
 206              </p>
 207              <p className="text-muted-foreground">
 208                Alby Hub works with selected service providers (LSPs) which
 209                provide the best network connectivity and liquidity to receive
 210                payments.
 211              </p>
 212              <div>
 213                <h2 className="font-medium text-foreground mb-3">
 214                  Purchase Your First Channel
 215                </h2>
 216                <p>
 217                  A payment is required to purchase a channel from{" "}
 218                  <ExternalLink
 219                    to={lspChannelOffer.lspContactUrl}
 220                    className="underline"
 221                  >
 222                    {lspChannelOffer.lspName}
 223                  </ExternalLink>
 224                  . Once your channel is opened, you'll immediately be able to
 225                  receive and send bitcoin with your Hub.
 226                </p>
 227              </div>
 228  
 229              {lspChannelOffer.currentPaymentMethod !== "prepaid" &&
 230                lspChannelOffer.currentPaymentMethod !== "included" && (
 231                  <div>
 232                    <h3 className="font-medium text-sm text-foreground mb-3">
 233                      Payment method
 234                    </h3>
 235                    <RadioGroup
 236                      value={currentPaymentMethod}
 237                      onValueChange={(
 238                        newPaymentMethod: LSPChannelOfferPaymentMethod
 239                      ) => {
 240                        if (
 241                          newPaymentMethod !==
 242                          lspChannelOffer.currentPaymentMethod
 243                        ) {
 244                          openLink("https://getalby.com/payment_details");
 245                        }
 246                        setCurrentPaymentMethod(newPaymentMethod);
 247                      }}
 248                    >
 249                      <FieldLabel htmlFor="wallet">
 250                        <Field orientation="horizontal">
 251                          <RadioGroupItem
 252                            value={
 253                              "wallet" satisfies LSPChannelOfferPaymentMethod
 254                            }
 255                            id="wallet"
 256                          />
 257                          <FieldContent>
 258                            <FieldTitle>Bitcoin</FieldTitle>
 259                          </FieldContent>
 260                        </Field>
 261                      </FieldLabel>
 262                      <FieldLabel htmlFor="card">
 263                        <Field orientation="horizontal">
 264                          <RadioGroupItem
 265                            value={"card" satisfies LSPChannelOfferPaymentMethod}
 266                            id="card"
 267                          />
 268                          <FieldContent>
 269                            <FieldTitle>Credit / Debit Card</FieldTitle>
 270                          </FieldContent>
 271                        </Field>
 272                      </FieldLabel>
 273                    </RadioGroup>
 274                  </div>
 275                )}
 276              {showAdvanced && (
 277                <>
 278                  <div className="mt-2 flex items-top space-x-2">
 279                    <Checkbox
 280                      id="public-channel"
 281                      onCheckedChange={() => setPublic(!isPublic)}
 282                      className="mr-2"
 283                    />
 284                    <div className="grid gap-1.5 leading-none">
 285                      <Label
 286                        htmlFor="public-channel"
 287                        className="cursor-pointer text-foreground"
 288                      >
 289                        Public Channel
 290                      </Label>
 291                      <p className="text-xs text-muted-foreground">
 292                        Not recommended for most users.{" "}
 293                        <ExternalLink
 294                          className="underline"
 295                          to="https://guides.getalby.com/user-guide/alby-hub/faq/should-i-open-a-private-or-public-channel"
 296                        >
 297                          Learn more
 298                        </ExternalLink>
 299                      </p>
 300                    </div>
 301                  </div>
 302                </>
 303              )}
 304              {!showAdvanced && (
 305                <div>
 306                  <Button
 307                    type="button"
 308                    variant="link"
 309                    className="text-muted-foreground text-xs px-0"
 310                    onClick={() => setShowAdvanced((current) => !current)}
 311                  >
 312                    Advanced Options
 313                    <ChevronDownIcon className="size-4" />
 314                  </Button>
 315                </div>
 316              )}
 317  
 318              <Separator />
 319  
 320              <Table>
 321                <TableBody>
 322                  <TableRow className="border-0">
 323                    <TableCell className="px-3 align-top">
 324                      <div className="flex flex-1 items-center gap-1 text-sm">
 325                        You'll be able to receive up to{" "}
 326                        <TooltipProvider>
 327                          <Tooltip>
 328                            <TooltipTrigger>
 329                              <div className="flex flex-row items-center">
 330                                <InfoIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
 331                              </div>
 332                            </TooltipTrigger>
 333                            <TooltipContent className="max-w-sm">
 334                              The amount you will be able to receive when funds
 335                              are on your counterparty's side of the channel.
 336                            </TooltipContent>
 337                          </Tooltip>
 338                        </TooltipProvider>
 339                      </div>
 340                    </TableCell>
 341                    <TableCell className="px-3 flex flex-col gap-2 items-end justify-center align-top">
 342                      {/* <span>
 343                        <FormattedBitcoinAmount
 344                          amountMsat={lspChannelOffer.lspBalanceSat * 1000}
 345                        />
 346                      </span> */}
 347                      <FormattedFiatAmount
 348                        amountSat={lspChannelOffer.lspBalanceSat}
 349                        showApprox
 350                      />
 351                    </TableCell>
 352                  </TableRow>
 353                  <TableRow className="border-0">
 354                    <TableCell className="p-3 text-sm">
 355                      Channel Cost
 356                      {lspChannelOffer.currentPaymentMethod === "included" &&
 357                        " (Included in your plan)"}
 358                    </TableCell>
 359                    <TableCell className="p-3 flex flex-col gap-2 items-end justify-center">
 360                      <p>
 361                        <span
 362                          className={cn(
 363                            lspChannelOffer.currentPaymentMethod === "included" &&
 364                              "line-through"
 365                          )}
 366                        >
 367                          {new Intl.NumberFormat(undefined, {
 368                            style: "currency",
 369                            currency: "USD",
 370                          }).format(lspChannelOffer.feeTotalUsd / 100)}
 371                        </span>
 372                        {lspChannelOffer.currentPaymentMethod === "included" && (
 373                          <span> $0.00</span>
 374                        )}
 375                      </p>
 376                    </TableCell>
 377                  </TableRow>
 378                </TableBody>
 379              </Table>
 380  
 381              <div className="w-full flex items-center justify-center -mb-2">
 382                <p className="text-center text-xs max-w-md">
 383                  By continuing, you consent to the{" "}
 384                  <LSPTermsDialog
 385                    contactUrl={lspChannelOffer.lspContactUrl}
 386                    description={lspChannelOffer.lspDescription}
 387                    name={lspChannelOffer.lspName}
 388                    terms={lspChannelOffer.terms}
 389                    trigger={<span className="underline">LSP terms</span>}
 390                  />{" "}
 391                  and that the channel opens immediately and that you lose the
 392                  right to revoke once it is open.
 393                </p>
 394              </div>
 395  
 396              <LoadingButton loading={isLoading} onClick={openChannel}>
 397                {lspChannelOffer.currentPaymentMethod === "prepaid" ? (
 398                  <>Review Order</>
 399                ) : lspChannelOffer.currentPaymentMethod === "included" ? (
 400                  <>Open Channel</>
 401                ) : (
 402                  <>
 403                    Pay{" "}
 404                    {new Intl.NumberFormat(undefined, {
 405                      style: "currency",
 406                      currency: "USD",
 407                    }).format(lspChannelOffer.feeTotalUsd / 100)}{" "}
 408                    and Open Channel
 409                  </>
 410                )}
 411              </LoadingButton>
 412            </div>
 413          </>
 414        )}
 415      </>
 416    );
 417  }
 418