SubwalletCreated.tsx raw
1 import {
2 AlertCircleIcon,
3 AlertTriangleIcon,
4 CopyIcon,
5 ExternalLinkIcon,
6 MonitorIcon,
7 RadioIcon,
8 SmartphoneIcon,
9 TriangleAlertIcon,
10 UserIcon,
11 ZapIcon,
12 } from "lucide-react";
13 import React from "react";
14 import { useLocation, useNavigate } from "react-router";
15 import AppHeader from "src/components/AppHeader";
16 import ExternalLink from "src/components/ExternalLink";
17 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
18 import { AppleIcon } from "src/components/icons/Apple";
19 import { PlayStoreIcon } from "src/components/icons/PlayStore";
20 import { ZapStoreIcon } from "src/components/icons/ZapStore";
21 import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog";
22 import QRCode from "src/components/QRCode";
23 import {
24 Accordion,
25 AccordionContent,
26 AccordionItem,
27 AccordionTrigger,
28 } from "src/components/ui/accordion";
29 import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
30 import { Button } from "src/components/ui/button";
31 import {
32 Card,
33 CardContent,
34 CardDescription,
35 CardFooter,
36 CardHeader,
37 CardTitle,
38 } from "src/components/ui/card";
39 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
40 import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment";
41 import { LinkButton } from "src/components/ui/custom/link-button";
42 import { LoadingButton } from "src/components/ui/custom/loading-button";
43 import { Input } from "src/components/ui/input";
44 import { Label } from "src/components/ui/label";
45 import {
46 Popover,
47 PopoverContent,
48 PopoverTrigger,
49 } from "src/components/ui/popover";
50 import { Textarea } from "src/components/ui/textarea";
51 import { UpgradeDialog } from "src/components/UpgradeDialog";
52 import { useAlbyMe } from "src/hooks/useAlbyMe";
53 import { useApp } from "src/hooks/useApp";
54 import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
55 import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo";
56 import { copyToClipboard } from "src/lib/clipboard";
57 import { ConnectAppCard } from "src/screens/apps/ConnectAppCard";
58 import { CreateAppResponse } from "src/types";
59
60 export function SubwalletCreated() {
61 const { data: nodeConnectionInfo } = useNodeConnectionInfo();
62
63 const { state } = useLocation();
64 const navigate = useNavigate();
65 const createAppResponse = state as CreateAppResponse | undefined;
66 const { data: app } = useApp(createAppResponse?.id, true);
67 const [intendedLightningAddress, setIntendedLightningAddress] =
68 React.useState(
69 createAppResponse?.name
70 .toLowerCase()
71 .trim()
72 .replace(/\s+/g, "_")
73 .replace(/[^a-z0-9_]/g, "") || ""
74 );
75
76 const { data: albyMe } = useAlbyMe();
77
78 const { createLightningAddress, creatingLightningAddress } =
79 useCreateLightningAddress(createAppResponse?.id);
80
81 const [step, setStep] = React.useState(1);
82
83 if (!createAppResponse?.pairingUri) {
84 navigate("/");
85 return null;
86 }
87
88 const name = createAppResponse.name;
89 let connectionSecret = createAppResponse.pairingUri;
90 if (app?.metadata?.lud16) {
91 connectionSecret += `&lud16=${app.metadata.lud16}`;
92 }
93
94 const albyAccountUrl = `https://getalby.com/nwc/new#${connectionSecret}`;
95 const valueTag = `<podcast:value type="lightning" method="keysend">
96 <podcast:valueRecipient name="${name}" type="node" address="${nodeConnectionInfo?.pubkey}" customKey="696969" customValue="${app?.id}" split="100"/>
97 </podcast:value>`;
98
99 return (
100 <div className="grid gap-5">
101 <AppHeader
102 pageTitle={`Connect ${name}`}
103 title={`Connect ${name}`}
104 description=""
105 />
106 <div className="max-w-lg">
107 <div className="flex flex-col col-span-3 gap-5 items-start">
108 {step === 1 && app && (
109 <div className="grid gap-5">
110 <div>
111 Configure this sub-wallet by creating a lightning address and
112 topping it up. That way it's ready to use as soon as it's
113 connected.
114 </div>
115 <div className="grid gap-3">
116 {!app.metadata?.lud16 && (
117 <Card>
118 <CardHeader>
119 <CardTitle>Lightning address</CardTitle>
120 <CardDescription>
121 Create a lightning address for this sub-wallet
122 </CardDescription>
123 </CardHeader>
124 <CardContent>
125 <InputWithAdornment
126 type="text"
127 value={intendedLightningAddress}
128 onChange={(e) =>
129 setIntendedLightningAddress(e.target.value)
130 }
131 required
132 autoComplete="off"
133 endAdornment={
134 <span className="mr-1 text-muted-foreground text-xs">
135 @getalby.com
136 </span>
137 }
138 />
139 </CardContent>
140 <CardFooter className="flex flex-row justify-end">
141 {!albyMe?.subscription.plan_code ? (
142 <UpgradeDialog>
143 <Button size="sm" variant="secondary">
144 Create Lightning Address
145 </Button>
146 </UpgradeDialog>
147 ) : (
148 <LoadingButton
149 loading={creatingLightningAddress}
150 onClick={() =>
151 createLightningAddress(intendedLightningAddress)
152 }
153 size="sm"
154 variant="secondary"
155 >
156 Create Lightning Address
157 </LoadingButton>
158 )}
159 </CardFooter>
160 </Card>
161 )}
162 {app.metadata?.lud16 && (
163 <Card>
164 <CardHeader>
165 <CardTitle>Lightning address</CardTitle>
166 <CardDescription>
167 Your lightning address for this sub-account
168 </CardDescription>
169 </CardHeader>
170 <CardContent>
171 <p className="font-semibold">{app.metadata.lud16}</p>
172 </CardContent>
173 <CardFooter className="flex flex-row justify-end">
174 <Button
175 onClick={() => {
176 if (app.metadata?.lud16) {
177 copyToClipboard(app.metadata.lud16);
178 }
179 }}
180 size="sm"
181 variant="secondary"
182 >
183 Copy
184 </Button>
185 </CardFooter>
186 </Card>
187 )}
188 <Card>
189 <CardHeader>
190 <CardTitle>{name}</CardTitle>
191 <CardDescription>
192 Balance:{" "}
193 <FormattedBitcoinAmount amountMsat={app.balanceMsat} />
194 </CardDescription>
195 </CardHeader>
196 <CardFooter className="flex flex-row justify-end">
197 <IsolatedAppTopupDialog appId={app.id}>
198 <Button size="sm" variant="secondary">
199 Top Up
200 </Button>
201 </IsolatedAppTopupDialog>
202 </CardFooter>
203 </Card>
204 <Button onClick={() => setStep(2)}>Next</Button>
205 </div>
206 </div>
207 )}
208 {step === 2 && (
209 <div className="grid gap-3">
210 <div>
211 Select which apps to connect to this sub-wallet — whether for
212 yourself or someone you're inviting. Connect to as many services
213 as you want:
214 </div>
215 <Alert variant="destructive">
216 <AlertCircleIcon className="h-4 w-4" />
217 <AlertTitle>Important</AlertTitle>
218 <AlertDescription className="inline">
219 For your security, these connection details are only visible
220 now and{" "}
221 <span className="font-semibold">
222 cannot be retrieved later
223 </span>
224 . If needed, you can store them in a password manager for
225 future reference.
226 </AlertDescription>
227 </Alert>
228 <Accordion type="single" collapsible defaultValue="other">
229 <AccordionItem value="other">
230 <AccordionTrigger>
231 <div className="flex items-center gap-2">
232 <ZapIcon className="h-4 w-4" />
233 Connect Your App
234 </div>
235 </AccordionTrigger>
236 <AccordionContent>
237 {app && (
238 <div className="flex justify-center">
239 <ConnectAppCard
240 app={app}
241 pairingUri={connectionSecret}
242 />
243 </div>
244 )}
245 </AccordionContent>
246 </AccordionItem>
247 <AccordionItem value="mobile">
248 <AccordionTrigger>
249 <div className="flex items-center gap-2">
250 <SmartphoneIcon className="h-4 w-4" />
251 Alby Go
252 </div>
253 </AccordionTrigger>
254 <AccordionContent>
255 <ul className="flex flex-col gap-1 list-inside list-decimal mb-6">
256 <li>
257 Download Alby Go from the app store
258 <div className="flex flex-row gap-3 my-2">
259 <Popover>
260 <Button variant="outline" asChild>
261 <PopoverTrigger>
262 <PlayStoreIcon />
263 Play Store
264 </PopoverTrigger>
265 </Button>
266 <PopoverContent className="flex flex-col items-center gap-3">
267 <QRCode value="https://play.google.com/store/apps/details?id=com.getalby.mobile" />
268 <ExternalLinkButton
269 variant="link"
270 to="https://play.google.com/store/apps/details?id=com.getalby.mobile"
271 >
272 Open
273 <ExternalLinkIcon />
274 </ExternalLinkButton>
275 </PopoverContent>
276 </Popover>
277 <Popover>
278 <Button variant="outline" asChild>
279 <PopoverTrigger>
280 <AppleIcon />
281 Apple App Store
282 </PopoverTrigger>
283 </Button>
284 <PopoverContent className="flex flex-col items-center gap-3">
285 <QRCode value="https://apps.apple.com/us/app/alby-go/id6471335774" />
286 <ExternalLinkButton
287 variant="link"
288 to="https://apps.apple.com/us/app/alby-go/id6471335774"
289 >
290 Open
291 <ExternalLinkIcon />
292 </ExternalLinkButton>
293 </PopoverContent>
294 </Popover>
295 <Popover>
296 <Button variant="outline" asChild>
297 <PopoverTrigger>
298 <ZapStoreIcon />
299 Zapstore
300 </PopoverTrigger>
301 </Button>
302 <PopoverContent className="flex flex-col items-center gap-3">
303 <div className="text-center text-xs text-muted-foreground">
304 Install Zapstore on your Android device and
305 search for{" "}
306 <span className="font-semibold">Alby Go</span>
307 </div>
308 <QRCode value="https://zapstore.dev" />
309 <ExternalLinkButton
310 variant="link"
311 to="https://zapstore.dev"
312 >
313 Open
314 <ExternalLinkIcon />
315 </ExternalLinkButton>
316 </PopoverContent>
317 </Popover>
318 </div>
319 </li>
320 <li>Open Alby Go and scan this QR code</li>
321 </ul>
322 {app && (
323 <ConnectAppCard app={app} pairingUri={connectionSecret} />
324 )}
325 </AccordionContent>
326 </AccordionItem>
327 <AccordionItem value="account">
328 <AccordionTrigger>
329 <div className="flex items-center gap-2">
330 <UserIcon className="h-4 w-4" />
331 Alby Account
332 </div>
333 </AccordionTrigger>
334 <AccordionContent>
335 <ul className="flex flex-col gap-1 list-inside list-decimal mb-6">
336 <li>
337 If the recipient doesn't have an Alby Account yet, ask
338 them to create an Alby Account on getalby.com or send
339 them an{" "}
340 <ExternalLink
341 to="https://getalby.com/invite_codes"
342 className="underline"
343 >
344 invite
345 </ExternalLink>
346 </li>
347 <li>
348 Send your recipient this connection link after they've
349 created an Alby account. The link will automatically
350 connect the wallet to their account — no extra steps
351 needed.
352 </li>
353 </ul>
354 <div className="flex gap-2">
355 <Input
356 disabled
357 readOnly
358 type="password"
359 value={albyAccountUrl}
360 />
361 <Button
362 onClick={() => copyToClipboard(albyAccountUrl)}
363 variant="outline"
364 >
365 <CopyIcon />
366 Copy URL
367 </Button>
368 </div>
369 <Alert className="mt-5">
370 <AlertTriangleIcon />
371 <AlertTitle>
372 Use separate browsers or profiles when managing multiple
373 Alby accounts
374 </AlertTitle>
375 <AlertDescription>
376 Opening a connection URL while logged into a different
377 Alby account will override your currently connected
378 wallet.
379 </AlertDescription>
380 </Alert>
381 </AccordionContent>
382 </AccordionItem>
383 <AccordionItem value="extension">
384 <AccordionTrigger>
385 <div className="flex items-center gap-2">
386 <MonitorIcon className="h-4 w-4" />
387 Alby Browser Extension
388 </div>
389 </AccordionTrigger>
390 <AccordionContent>
391 <ul className="list-inside list-decimal flex flex-col gap-1 mb-6">
392 <li>
393 Install Alby Browser Extension from{" "}
394 <ExternalLink
395 className="underline"
396 to="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe"
397 >
398 Chrome Web Store
399 </ExternalLink>{" "}
400 or{" "}
401 <ExternalLink
402 className="underline"
403 to="https://addons.mozilla.org/en-US/firefox/addon/alby/"
404 >
405 Firefox Add-ons
406 </ExternalLink>
407 </li>
408 <li>
409 In the extension, choose{" "}
410 <span className="font-medium">
411 Bring Your Own Wallet
412 </span>{" "}
413 while connecting a wallet
414 </li>
415 <li>
416 Choose{" "}
417 <span className="font-medium">
418 Nostr Wallet Connect
419 </span>{" "}
420 option
421 </li>
422 <li>
423 Copy & paste the connection secret below to connect the
424 sub-wallet to the extension.
425 </li>
426 </ul>
427 <div className="flex gap-2">
428 <Input
429 disabled
430 readOnly
431 type="password"
432 value={connectionSecret}
433 />
434 <Button
435 onClick={() => copyToClipboard(connectionSecret)}
436 variant="outline"
437 >
438 <CopyIcon />
439 Copy
440 </Button>
441 </div>
442 </AccordionContent>
443 </AccordionItem>
444 <AccordionItem value="podcasting">
445 <AccordionTrigger>
446 <div className="flex items-center gap-2">
447 <RadioIcon className="h-4 w-4" />
448 Podcasting 2.0
449 </div>
450 </AccordionTrigger>
451 <AccordionContent>
452 <p className="text-muted-foreground text-sm mb-5">
453 To allow receiving podcasting 2.0 payments to the
454 sub-wallet, make sure to share the podcast:value tag that
455 needs to be added to recipient's RSS feed:
456 </p>
457 <div className="flex flex-col gap-2">
458 <div className="grid gap-1.5">
459 <Label htmlFor="podcastValueTag">
460 podcast:value tag
461 </Label>
462 <Textarea
463 id="podcastValueTag"
464 readOnly
465 className="h-32"
466 value={valueTag}
467 />
468 </div>
469 <Button
470 onClick={() => copyToClipboard(valueTag)}
471 variant="outline"
472 >
473 <CopyIcon />
474 Copy
475 </Button>
476 <Alert>
477 <AlertTitle className="flex flex-row gap-2">
478 <TriangleAlertIcon className="w-4 h-4" />
479 Make sure you also connect other options
480 </AlertTitle>
481 <AlertDescription>
482 This connection will allow them to receive payments
483 from their podcast audience. To allow them to spend
484 the funds, make sure they are connected to other
485 options like Alby Go or Alby Account.
486 </AlertDescription>
487 </Alert>
488 </div>
489 </AccordionContent>
490 </AccordionItem>
491 </Accordion>
492 <div className="flex gap-2">
493 <Button onClick={() => setStep(1)} variant="secondary">
494 Back
495 </Button>
496 <LinkButton to="/sub-wallets">Finish</LinkButton>
497 </div>
498 </div>
499 )}
500 </div>
501 </div>
502 </div>
503 );
504 }
505