SubwalletList.tsx raw
1 import {
2 CirclePlusIcon,
3 HelpCircleIcon,
4 InfoIcon,
5 ShieldCheckIcon,
6 SparklesIcon,
7 TriangleAlertIcon,
8 } from "lucide-react";
9 import { useRef, useState } from "react";
10 import AppHeader from "src/components/AppHeader";
11 import AppCard from "src/components/connections/AppCard";
12 import { CustomPagination } from "src/components/CustomPagination";
13 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
14 import FormattedFiatAmount from "src/components/FormattedFiatAmount";
15 import Loading from "src/components/Loading";
16 import ResponsiveButton from "src/components/ResponsiveButton";
17 import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
18 import {
19 Alert,
20 AlertAction,
21 AlertDescription,
22 AlertTitle,
23 } from "src/components/ui/alert";
24 import { Button } from "src/components/ui/button";
25 import {
26 Card,
27 CardContent,
28 CardHeader,
29 CardTitle,
30 } from "src/components/ui/card";
31 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
32 import { LinkButton } from "src/components/ui/custom/link-button";
33 import { UpgradeDialog } from "src/components/UpgradeDialog";
34 import { LIST_APPS_LIMIT, MAX_FREE_SUBWALLETS } from "src/constants";
35 import { useAlbyMe } from "src/hooks/useAlbyMe";
36 import { useApps } from "src/hooks/useApps";
37 import { useBalances } from "src/hooks/useBalances";
38 import { useInfo } from "src/hooks/useInfo";
39 import { SubwalletIntro } from "src/screens/subwallets/SubwalletIntro";
40
41 export function SubwalletList() {
42 const { data: info } = useInfo();
43 const [page, setPage] = useState(1);
44 const appsListRef = useRef<HTMLDivElement>(null);
45 const { data: subwalletAppsData } = useApps(
46 undefined,
47 page,
48 {
49 subWallets: true,
50 },
51 "created_at"
52 );
53 const { data: albyMe, error: albyMeError } = useAlbyMe();
54 const { data: balances } = useBalances();
55
56 const handlePageChange = (page: number) => {
57 setPage(page);
58 appsListRef.current?.scrollIntoView({
59 behavior: "smooth",
60 block: "start",
61 });
62 };
63
64 if (
65 !info ||
66 !subwalletAppsData ||
67 !balances ||
68 (info.albyAccountConnected && !albyMe && !albyMeError)
69 ) {
70 return <Loading />;
71 }
72
73 const subwalletApps = subwalletAppsData.apps;
74
75 if (!subwalletAppsData.totalCount) {
76 return <SubwalletIntro />;
77 }
78
79 const subwalletTotalAmountMsat = subwalletAppsData.totalBalanceMsat || 0;
80 const isSufficientlyBacked =
81 subwalletTotalAmountMsat <= balances.lightning.totalSpendableMsat;
82
83 return (
84 <div className="grid gap-3">
85 <AppHeader
86 title="Sub-wallets"
87 pageTitle="Sub-wallets"
88 description="Create sub-wallets for yourself, friends, family or coworkers"
89 contentRight={
90 <>
91 <ExternalLinkButton
92 to="https://guides.getalby.com/user-guide/alby-hub/sub-wallets"
93 variant="outline"
94 size="icon"
95 >
96 <HelpCircleIcon className="size-4" />
97 </ExternalLinkButton>
98 {!albyMe?.subscription.plan_code &&
99 subwalletAppsData.totalCount >= MAX_FREE_SUBWALLETS ? (
100 <UpgradeDialog>
101 <ResponsiveButton icon={CirclePlusIcon} text="New Sub-wallet" />
102 </UpgradeDialog>
103 ) : (
104 <ResponsiveLinkButton
105 to="/sub-wallets/new"
106 icon={CirclePlusIcon}
107 text="New Sub-wallet"
108 />
109 )}
110 </>
111 }
112 />
113
114 {!albyMe?.subscription.plan_code &&
115 subwalletAppsData.totalCount >= MAX_FREE_SUBWALLETS && (
116 <Alert>
117 <InfoIcon />
118 <AlertTitle>Need more Sub-wallets?</AlertTitle>
119 <AlertDescription>
120 Upgrade to Pro for unlimited sub-wallets.
121 </AlertDescription>
122 <AlertAction>
123 <UpgradeDialog>
124 <Button size="sm">
125 <SparklesIcon />
126 Upgrade
127 </Button>
128 </UpgradeDialog>
129 </AlertAction>
130 </Alert>
131 )}
132
133 {!isSufficientlyBacked && (
134 <Alert variant="warning">
135 <TriangleAlertIcon />
136 <AlertTitle>
137 Sub-wallets you manage are insufficiently backed
138 </AlertTitle>
139 <AlertDescription className="flex flex-row gap-3">
140 There's not enough bitcoin in your lightning balance to honor all
141 balances of sub-wallets under your management. Increase your
142 lightning balance by opening a channel or review your channel
143 statuses to back them up again.
144 <LinkButton to="/wallet/receive" variant="secondary">
145 Deposit Bitcoin
146 </LinkButton>
147 </AlertDescription>
148 </Alert>
149 )}
150
151 <div className="flex flex-col sm:flex-row flex-wrap gap-3 slashed-zero">
152 <Card className="flex flex-1 flex-col">
153 <CardHeader className="pb-2">
154 <CardTitle className="text-lg">
155 Total Balance of Sub-wallets
156 </CardTitle>
157 </CardHeader>
158 <CardContent className="grow">
159 <div className="mb-1">
160 <span className="text-2xl font-medium balance sensitive">
161 <FormattedBitcoinAmount amountMsat={subwalletTotalAmountMsat} />
162 </span>
163 </div>
164 <FormattedFiatAmount amountSat={subwalletTotalAmountMsat / 1000} />
165 </CardContent>
166 </Card>
167 <Card className="flex flex-1 flex-col">
168 <CardHeader className="pb-2">
169 <CardTitle className="text-lg">Number of Sub-wallets</CardTitle>
170 </CardHeader>
171 <CardContent className="grow flex flex-col gap-4">
172 <div className="flex flex-col gap-2">
173 <span className="text-2xl font-medium">
174 {subwalletAppsData.totalCount} /{" "}
175 {albyMe?.subscription.plan_code ? "∞" : MAX_FREE_SUBWALLETS}
176 </span>
177 {isSufficientlyBacked ? (
178 <div className="flex items-center text-positive-foreground text-sm">
179 <ShieldCheckIcon className="size-4 mr-2" />
180 <span className="text-sm font-medium">Fully backed</span>
181 </div>
182 ) : (
183 <div className="flex items-center text-warning-foreground text-sm">
184 <TriangleAlertIcon className="size-4 mr-2" />
185 <span className="text-sm font-medium">
186 Insufficiently backed
187 </span>
188 </div>
189 )}
190 </div>
191 </CardContent>
192 </Card>
193 </div>
194 <div className="mt-8">
195 <h3 className="font-semibold text-2xl mb-4">Managed Sub-wallets</h3>
196 <div
197 ref={appsListRef}
198 className="grid grid-cols-1 lg:grid-cols-2 gap-3 items-stretch"
199 >
200 {subwalletApps.map((app, index) => (
201 <AppCard key={index} app={app} />
202 ))}
203 </div>
204 </div>
205
206 <CustomPagination
207 limit={LIST_APPS_LIMIT}
208 totalCount={subwalletAppsData.totalCount}
209 page={page}
210 handlePageChange={handlePageChange}
211 />
212 </div>
213 );
214 }
215