diff --git a/frontend/src/hooks/useChannels.ts b/frontend/src/hooks/useChannels.ts index 75438652..46071326 100644 --- a/frontend/src/hooks/useChannels.ts +++ b/frontend/src/hooks/useChannels.ts @@ -7,9 +7,9 @@ const pollConfiguration: SWRConfiguration = { refreshInterval: 3000, }; -export function useChannels(poll = false) { +export function useChannels(poll = false, enabled = true) { return useSWR( - "/api/channels", + enabled ? "/api/channels" : undefined, swrFetcher, poll ? pollConfiguration : undefined ); diff --git a/frontend/src/hooks/useLSPChannelOffer.tsx b/frontend/src/hooks/useLSPChannelOffer.tsx index 143a5751..1990c056 100644 --- a/frontend/src/hooks/useLSPChannelOffer.tsx +++ b/frontend/src/hooks/useLSPChannelOffer.tsx @@ -3,6 +3,9 @@ import useSWR from "swr"; import { LSPChannelOffer } from "src/types"; import { swrFetcher } from "src/utils/swr"; -export function useLSPChannelOffer() { - return useSWR("/api/channel-offer", swrFetcher); +export function useLSPChannelOffer(enabled = true) { + return useSWR( + enabled ? "/api/channel-offer" : undefined, + swrFetcher + ); } diff --git a/frontend/src/screens/channels/first/FirstChannel.tsx b/frontend/src/screens/channels/first/FirstChannel.tsx index c3fed0c7..04d3e200 100644 --- a/frontend/src/screens/channels/first/FirstChannel.tsx +++ b/frontend/src/screens/channels/first/FirstChannel.tsx @@ -1,84 +1,150 @@ -import { ChevronDownIcon, InfoIcon } from "lucide-react"; +import { CreditCardIcon } from "lucide-react"; import React from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import ExternalLink from "src/components/ExternalLink"; -import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; +import { MempoolAlert } from "src/components/MempoolAlert"; +import { PayLightningInvoice } from "src/components/PayLightningInvoice"; +import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; +import { LSPTermsDialog } from "src/components/channels/LSPTermsDialog"; import { Button } from "src/components/ui/button"; import { Checkbox } from "src/components/ui/checkbox"; import { LoadingButton } from "src/components/ui/custom/loading-button"; -import { Label } from "src/components/ui/label"; -import { Separator } from "src/components/ui/separator"; -import { useChannels } from "src/hooks/useChannels"; - -import { useInfo } from "src/hooks/useInfo"; -import { - AutoChannelRequest, - AutoChannelResponse, - LSPChannelOfferPaymentMethod, -} from "src/types"; -import { request } from "src/utils/request"; - -import { Invoice } from "@getalby/lightning-tools"; -import { MempoolAlert } from "src/components/MempoolAlert"; -import { PayLightningInvoice } from "src/components/PayLightningInvoice"; -import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table"; - -import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg"; -import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg"; -import { LSPTermsDialog } from "src/components/channels/LSPTermsDialog"; -import FormattedFiatAmount from "src/components/FormattedFiatAmount"; -import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LinkButton } from "src/components/ui/custom/link-button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "src/components/ui/dialog"; import { Field, FieldContent, FieldLabel, FieldTitle, } from "src/components/ui/field"; +import { Label } from "src/components/ui/label"; import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "src/components/ui/tooltip"; +import { Separator } from "src/components/ui/separator"; +import { useChannels } from "src/hooks/useChannels"; +import { useInfo } from "src/hooks/useInfo"; import { useLSPChannelOffer } from "src/hooks/useLSPChannelOffer"; import { cn } from "src/lib/utils"; +import { + AutoChannelRequest, + AutoChannelResponse, + LSPChannelOffer, + LSPChannelOfferPaymentMethod, +} from "src/types"; import { openLink } from "src/utils/openLink"; +import { request } from "src/utils/request"; -export function FirstChannel() { - const { data: info } = useInfo(); - const { data: channels } = useChannels(true); +import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg"; +import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg"; + +type FirstChannelProps = { + variant?: "app" | "setup"; +}; + +const DEFAULT_PAYMENT_METHOD = "wallet" satisfies LSPChannelOfferPaymentMethod; +const PAYMENT_DETAILS_URL = "https://getalby.com/payment_details"; + +export function FirstChannel({ variant = "app" }: FirstChannelProps) { + const isSetup = variant === "setup"; + const { data: info, hasChannelManagement } = useInfo(); + const shouldLoadChannelData = + !info || (!isSetup && !info.albyAccountConnected) + ? true + : Boolean(hasChannelManagement && info.albyAccountConnected); + const { data: channels } = useChannels(true, shouldLoadChannelData); + const { data: lspChannelOffer } = useLSPChannelOffer( + shouldLoadChannelData && Boolean(info?.albyAccountConnected) + ); const [isLoading, setLoading] = React.useState(false); const [showAdvanced, setShowAdvanced] = React.useState(false); const [isPublic, setPublic] = React.useState(false); - const { data: lspChannelOffer } = useLSPChannelOffer(); const navigate = useNavigate(); const [invoice, setInvoice] = React.useState(); const [channelSizeSat, setChannelSizeSat] = React.useState(); const [currentPaymentMethod, setCurrentPaymentMethod] = - React.useState(); + React.useState(DEFAULT_PAYMENT_METHOD); + + const openingPath = isSetup + ? "/setup/first-channel/opening" + : "/channels/first/opening"; React.useEffect(() => { if (channels?.length) { - navigate("/channels/first/opening"); + navigate(openingPath); } - }, [channels, navigate]); + }, [channels, navigate, openingPath]); React.useEffect(() => { - if (info && !info.albyAccountConnected) { + if (!isSetup && info && !info.albyAccountConnected) { navigate("/channels/incoming"); } - }, [info, navigate]); + }, [info, isSetup, navigate]); - if (!info?.albyAccountConnected || !channels || !lspChannelOffer) { + React.useEffect(() => { + if (isSetup && info && !info.albyAccountConnected) { + navigate("/home", { replace: true }); + } + }, [info, isSetup, navigate]); + + React.useEffect(() => { + if (!lspChannelOffer) { + return; + } + if ( + lspChannelOffer.currentPaymentMethod === "card" || + lspChannelOffer.currentPaymentMethod === "wallet" + ) { + setCurrentPaymentMethod(lspChannelOffer.currentPaymentMethod); + } + }, [lspChannelOffer]); + + if (!info || (shouldLoadChannelData && !channels)) { return ; } + if (isSetup && !hasChannelManagement) { + return ( +
+ + + Go to dashboard + +
+ ); + } + + if (!info.albyAccountConnected) { + if (isSetup) { + return null; + } + return ; + } + + if (!lspChannelOffer) { + return ; + } + + const showPaymentMethod = + lspChannelOffer.currentPaymentMethod !== "prepaid" && + lspChannelOffer.currentPaymentMethod !== "included"; + const needsPaymentCard = + currentPaymentMethod === "card" && + lspChannelOffer.currentPaymentMethod !== "card"; + async function openChannel() { if (!info || !channels || !lspChannelOffer) { return; @@ -88,6 +154,9 @@ export function FirstChannel() { lspChannelOffer.currentPaymentMethod === "wallet") && currentPaymentMethod !== lspChannelOffer.currentPaymentMethod ) { + if (currentPaymentMethod === "card") { + openLink(PAYMENT_DETAILS_URL); + } toast.error("Payment method incorrectly configured", { description: currentPaymentMethod ? "Please switch the payment method and confirm the change in your Alby Account settings" @@ -125,293 +194,345 @@ export function FirstChannel() { return ( <> - - - {invoice && channelSizeSat && ( -
-

- Alby Hub works with selected service providers (LSPs) which provide - the best network connectivity and liquidity to receive payments. To - quickly get started you can buy a channel from an LSP by paying the - lightning invoice below. -

-
- - - - - Incoming Liquidity - - - - - - {invoice && ( - - - Amount to pay - - - - - - )} - -
-
- - - -

- Other options -

- - Open Channel with On-Chain Bitcoin - - - Buy Bitcoin - -
+ {isSetup ? ( + Open Lightning Channel ยท Alby Hub + ) : ( + )} - {!invoice && ( - <> -
- + ) : ( +
+ + + {showPaymentMethod && ( + - -

- You're now going to open your first lightning channel and can - begin using your Hub in the booming bitcoin economy! -

-

- Alby Hub works with selected service providers (LSPs) which - provide the best network connectivity and liquidity to receive - payments. -

-
-

- Purchase Your First Channel -

-

- A payment is required to purchase a channel from{" "} - - {lspChannelOffer.lspName} - - . Once your channel is opened, you'll immediately be able to - receive and send bitcoin with your Hub. -

+ )} + + {needsPaymentCard && } + + {!showAdvanced && ( +
+
+ )} - {lspChannelOffer.currentPaymentMethod !== "prepaid" && - lspChannelOffer.currentPaymentMethod !== "included" && ( -
-

- Payment method -

- { - if ( - newPaymentMethod !== - lspChannelOffer.currentPaymentMethod - ) { - openLink("https://getalby.com/payment_details"); - } - setCurrentPaymentMethod(newPaymentMethod); - }} - > - - - - - Bitcoin - - - - - - - - Credit / Debit Card - - - - -
- )} - {showAdvanced && ( - <> -
- setPublic(!isPublic)} - className="mr-2" - /> -
- -

- Not recommended for most users.{" "} - - Learn more - -

-
-
- - )} - {!showAdvanced && ( -
- -
- )} + {showAdvanced && ( + + )} - + - - - - -
- You'll be able to receive up to{" "} - - - -
- -
-
- - The amount you will be able to receive when funds - are on your counterparty's side of the channel. - -
-
-
-
- - {/* - - */} - - -
- - - Channel Cost - {lspChannelOffer.currentPaymentMethod === "included" && - " (Included in your plan)"} - - -

- - {new Intl.NumberFormat(undefined, { - style: "currency", - currency: "USD", - }).format(lspChannelOffer.feeTotalUsd / 100)} - - {lspChannelOffer.currentPaymentMethod === "included" && ( - $0.00 - )} -

-
-
-
-
- -
-

- By continuing, you consent to the{" "} - LSP terms} - />{" "} - and that the channel opens immediately and that you lose the - right to revoke once it is open. -

-
+ +
- {lspChannelOffer.currentPaymentMethod === "prepaid" ? ( - <>Review Order - ) : lspChannelOffer.currentPaymentMethod === "included" ? ( - <>Open Channel - ) : ( - <> - Pay{" "} - {new Intl.NumberFormat(undefined, { - style: "currency", - currency: "USD", - }).format(lspChannelOffer.feeTotalUsd / 100)}{" "} - and Open Channel - - )} + {getOpenChannelCta(lspChannelOffer, currentPaymentMethod)} + {isSetup && ( + + Skip for Now + + )}
- + +

+ By committing, you agree to the{" "} + LSP terms} + /> + . +

+
)} ); } + +function FirstChannelIntro({ + lspChannelOffer, +}: { + lspChannelOffer: LSPChannelOffer; +}) { + return ( + <> + +
+

+ Open Lightning Channel +

+

+ Instantly open a lightning channel to another network node with our + partner{" "} + + {lspChannelOffer.lspName} + + . Once opened, you'll be able to send and receive bitcoin instantly.{" "} + +

+
+ + ); +} + +function PaymentMethodSelector({ + currentPaymentMethod, + onPaymentMethodChange, +}: { + currentPaymentMethod: LSPChannelOfferPaymentMethod; + onPaymentMethodChange(paymentMethod: LSPChannelOfferPaymentMethod): void; +}) { + return ( +
+

Payment method

+ + onPaymentMethodChange(newPaymentMethod) + } + > + + + + + Bitcoin + + + + + + + + + Credit / Debit Card + + + + + +
+ ); +} + +function AddPaymentCard() { + return ( +
+
+ +
+
+

+ Add a payment card +

+

+ Save your card in Alby Account settings to pay for the channel. +

+
+ +
+ ); +} + +function PublicChannelOption({ + isPublic, + setPublic, +}: { + isPublic: boolean; + setPublic(isPublic: boolean): void; +}) { + return ( +
+ setPublic(checked === true)} + /> +
+ +

+ Not recommended for most users.{" "} + + Learn more + +

+
+
+ ); +} + +function ChannelSummary({ + lspChannelOffer, +}: { + lspChannelOffer: LSPChannelOffer; +}) { + return ( +
+
+

+ You'll be able to receive up to: +

+ +
+
+

Total channel cost:

+

+ + {new Intl.NumberFormat(undefined, { + style: "currency", + currency: "USD", + }).format(lspChannelOffer.feeTotalUsd / 100)} + + {lspChannelOffer.currentPaymentMethod === "included" && ( + $0.00 + )} +

+
+
+ ); +} + +function FirstChannelPayment({ + channelSizeSat, + invoice, + lspChannelOffer, +}: { + channelSizeSat: number; + invoice: string; + lspChannelOffer: LSPChannelOffer; +}) { + return ( +
+

Open First Channel

+ +
+ ); +} + +function LearnMoreDialog() { + return ( + + + + + + + Lightning Network Channels + +
+

+ Lightning channels allow you to send and receive bitcoin instantly + on the lightning network. Each channel has a certain capacity of how + much bitcoin it can receive. +

+

+ Alby Hub opens lightning channels to selected liquidity providers + (LSPs) for optimal network connectivity and reliability. +

+
+ + +
+
+
+
+ ); +} + +function getOpenChannelCta( + lspChannelOffer: LSPChannelOffer, + currentPaymentMethod: LSPChannelOfferPaymentMethod +) { + if (lspChannelOffer.currentPaymentMethod === "prepaid") { + return "Review Order"; + } + if (lspChannelOffer.currentPaymentMethod === "included") { + return "Open Channel"; + } + if (currentPaymentMethod === "card") { + return `Pay ${new Intl.NumberFormat(undefined, { + style: "currency", + currency: "USD", + }).format(lspChannelOffer.feeTotalUsd / 100)} and Open Channel`; + } + return "Review Order"; +}