Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

feat: new account link card #420

Merged
merged 12 commits into from
Jun 13, 2024
159 changes: 159 additions & 0 deletions frontend/src/components/AlbyConnectionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar";
import { Separator } from "@radix-ui/react-dropdown-menu";
import { Progress } from "@radix-ui/react-progress";
import {
CheckCircle2,
CircleX,
Edit,
ExternalLinkIcon,
Link2Icon,
ZapIcon,
} from "lucide-react";
import { Link } from "react-router-dom";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import { Button } from "src/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "src/components/ui/card";
import { LoadingButton } from "src/components/ui/loading-button";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { LinkStatus, useLinkAccount } from "src/hooks/useLinkAccount";
import { App } from "src/types";

function AlbyConnectionCard({ connection }: { connection?: App }) {
const { data: albyMe } = useAlbyMe();
const { loading, linkStatus, loadingLinkStatus, linkAccount } =
useLinkAccount();

return (
<Card>
<CardHeader>
<CardTitle>Alby Account</CardTitle>
<CardDescription>
Link Your Alby Account to use your lightning address with Alby Hub and
use apps that you connected to your Alby Account.
</CardDescription>
</CardHeader>
<Separator />
<CardContent>
<div className="grid grid-cols-1 xl:grid-cols-2 mt-5 gap-3 items-center">
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-4 ">
<Avatar className="h-14 w-14">
<AvatarImage src={albyMe?.avatar} alt="@satoshi" />
<AvatarFallback>SN</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<div className="text-xl font-semibold">{albyMe?.name}</div>
<div className="flex flex-row items-center gap-1">
<ZapIcon className="w-4 h-4" />
{albyMe?.lightning_address}
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 sm:items-center">
{loadingLinkStatus && <Loading />}
{!connection || linkStatus === LinkStatus.SharedNode ? (
<LoadingButton onClick={linkAccount} loading={loading}>
{!loading && <Link2Icon className="w-4 h-4 mr-2" />}
Link your Alby Account
</LoadingButton>
) : linkStatus === LinkStatus.ThisNode ? (
<Button
variant="positive"
disabled
className="disabled:opacity-100"
>
<CheckCircle2 className="w-4 h-4 mr-2" />
Alby Account Linked
</Button>
) : (
linkStatus === LinkStatus.OtherNode && (
<Button variant="destructive" disabled>
<CircleX className="w-4 h-4 mr-2" />
Linked to another wallet
</Button>
)
)}
<ExternalLink
to="https://www.getalby.com/node"
className="w-full md:w-auto"
>
<Button variant="outline" className="w-full md:w-auto">
<ExternalLinkIcon className="w-4 h-4 mr-2" />
Alby Account Settings
</Button>
</ExternalLink>
</div>
</div>
<div>
{connection && (
<>
{connection.maxAmount > 0 && (
<>
<div className="flex flex-row justify-between">
<div className="mb-2">
<p className="text-xs text-secondary-foreground font-medium">
You've spent
</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(
connection.budgetUsage
)}{" "}
sats
</p>
</div>
<div className="text-right">
{" "}
<p className="text-xs text-secondary-foreground font-medium">
Left in budget
</p>
<p className="text-xl font-medium text-muted-foreground">
{new Intl.NumberFormat().format(
connection.maxAmount - connection.budgetUsage
)}{" "}
sats
</p>
</div>
</div>
<Progress
className="h-4"
value={
(connection.budgetUsage * 100) / connection.maxAmount
}
/>
<div className="flex flex-row justify-between text-xs items-center mt-2">
{connection.maxAmount > 0 ? (
<>
{new Intl.NumberFormat().format(connection.maxAmount)}{" "}
sats / {connection.budgetRenewal}
</>
) : (
"Not set"
)}
<div>
<Link to={`/apps/${connection.nostrPubkey}`}>
<Button variant="ghost" size="sm">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
</Link>
</div>
</div>
</>
)}
</>
)}
</div>
</div>
</CardContent>
</Card>
);
}

export default AlbyConnectionCard;
2 changes: 1 addition & 1 deletion frontend/src/components/SidebarHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function SidebarHint() {
title="Link your Hub"
description="Finish the setup by linking your Alby Account to this hub."
buttonText="Link Hub"
buttonLink="/settings"
buttonLink="/apps"
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const buttonVariants = cva(
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
positive:
"bg-positive text-positive-foreground shadow-sm hover:bg-positive/90",
},
size: {
default: "h-9 px-4 py-2",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/ui/loading-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2",
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/hooks/useLinkAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState } from "react";
import { toast } from "src/components/ui/use-toast";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useCSRF } from "src/hooks/useCSRF";
import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo";
import { request } from "src/utils/request";

export enum LinkStatus {
SharedNode,
ThisNode,
OtherNode,
}

export function useLinkAccount() {
const { data: csrf } = useCSRF();
const { data: me, mutate: reloadAlbyMe } = useAlbyMe();
const { data: nodeConnectionInfo } = useNodeConnectionInfo();
const [loading, setLoading] = useState(false);

let linkStatus: LinkStatus | undefined;
if (me && nodeConnectionInfo) {
if (me?.keysend_pubkey === nodeConnectionInfo.pubkey) {
linkStatus = LinkStatus.ThisNode;
} else if (me.shared_node) {
linkStatus = LinkStatus.SharedNode;
} else {
linkStatus = LinkStatus.OtherNode;
}
}

const loadingLinkStatus = !linkStatus;

async function linkAccount() {
try {
setLoading(true);
if (!csrf) {
throw new Error("csrf not loaded");
}
await request("/api/alby/link-account", {
method: "POST",
headers: {
"X-CSRF-Token": csrf,
"Content-Type": "application/json",
},
});
await reloadAlbyMe();
toast({
title:
"Your Alby Hub has successfully been linked to your Alby Account",
});
} catch (e) {
toast({
title: "Your Alby Hub couldn't be linked to your Alby Account",
description: "Did you already link another Alby Hub?",
});
} finally {
setLoading(false);
}
}

return { loading, loadingLinkStatus, linkStatus, linkAccount };
}
2 changes: 2 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
--positive: 138, 68%, 96%;
--positive-foreground: 142 76% 36%;
}

.dark {
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/screens/apps/AppList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Cable, CirclePlus } from "lucide-react";
import { Link } from "react-router-dom";
import AlbyConnectionCard from "src/components/AlbyConnectionCard";
import AppCard from "src/components/AppCard";
import AppHeader from "src/components/AppHeader";
import EmptyState from "src/components/EmptyState";
Expand All @@ -8,6 +9,8 @@ import { Button } from "src/components/ui/button";
import { useApps } from "src/hooks/useApps";
import { useInfo } from "src/hooks/useInfo";

const albyConnectionName = "getalby.com";

function AppList() {
const { data: apps } = useApps();
const { data: info } = useInfo();
Expand All @@ -16,6 +19,11 @@ function AppList() {
return <Loading />;
}

const albyConnection = apps.find((x) => x.name === albyConnectionName);
const otherApps = apps.filter(
(x) => x.nostrPubkey !== albyConnection?.nostrPubkey
);

return (
<>
<AppHeader
Expand All @@ -31,7 +39,9 @@ function AppList() {
}
/>

{!apps.length && (
<AlbyConnectionCard connection={albyConnection} />

{!otherApps.length && (
<EmptyState
icon={Cable}
title="Connect Your First App"
Expand All @@ -41,9 +51,9 @@ function AppList() {
/>
)}

{apps.length > 0 && (
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
{apps.map((app, index) => (
{otherApps.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{otherApps.map((app, index) => (
<AppCard key={index} app={app} />
))}
</div>
Expand Down
Loading
Loading