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