Migrate In-app wallet users page to shadcn/tailwind (#4348)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor and optimize components in the dashboard app.

### Detailed summary
- Updated rendering of supported platforms in `ConnectLanding`
- Refactored `SupportedPlatformLink` component
- Improved UI and functionality in `Analytics` and `UsageCard`
- Enhanced user experience in `index.tsx` for embedded wallets

> The following files were skipped due to too many changes: `apps/dashboard/src/components/embedded-wallets/Users/index.tsx`, `apps/dashboard/src/pages/dashboard/connect/in-app-wallets.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
MananTank
2024-09-02 11:34:21 +00:00
parent d9177eb724
commit fa7281d4b7
7 changed files with 229 additions and 300 deletions
@@ -1,8 +1,9 @@
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
import { Box, Flex } from "@chakra-ui/react";
import { UsageCard } from "components/settings/Account/UsageCard";
import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import { Card, Heading, Text, TrackedLink } from "tw-components";
import { toNumber, toPercent } from "utils/number";
interface AnalyticsProps {
@@ -14,7 +15,7 @@ export const Analytics: React.FC<AnalyticsProps> = ({ trackingCategory }) => {
const walletsMetrics = useMemo(() => {
if (!usageQuery?.data) {
return {};
return undefined;
}
const usageData = usageQuery.data;
@@ -36,41 +37,39 @@ export const Analytics: React.FC<AnalyticsProps> = ({ trackingCategory }) => {
};
}, [usageQuery]);
if (usageQuery.isLoading || !usageQuery.data) {
return null;
}
return (
<Card p={{ base: 6, lg: 12 }}>
<Flex
flexDir={{ base: "column", lg: "row" }}
justifyContent="space-evenly"
gap={6}
>
<Flex flexDir="column" gap={2} justifyContent="center">
<Text size="label.sm">Analytics</Text>
<Heading size="title.sm" maxW="md" lineHeight={1.3}>
View more insights about how users are interacting with your
application
</Heading>
<div className="border border-border rounded-lg p-4 md:px-10 md:py-12 flex flex-col lg:flex-row lg:justify-between gap-6 bg-muted/50">
{/* Left */}
<div>
<p className="text-xs text-muted-foreground mb-3">Analytics</p>
<h2 className="text-2xl tracking-tight font-semibold max-w-[500px] mb-5">
View more insights about how users are interacting with your
application
</h2>
<TrackedLink
color="blue.500"
<Button asChild variant="outline" size="sm">
<TrackedLinkTW
href="/dashboard/connect/analytics"
category={trackingCategory}
label="view-analytics"
className="mt-auto min-w-[150px] gap-2"
>
View Analytics
</TrackedLink>
</Flex>
<Box minW={280}>
<ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
{/* Right */}
<div className="min-w-[280px]">
{walletsMetrics && (
<UsageCard
{...walletsMetrics}
name="Monthly Active Users"
tooltip="Email wallet (with managed recovery code) usage is calculated by monthly active wallets (i.e. active as defined by at least 1 user log-in via email or social within the billing period month)."
/>
</Box>
</Flex>
</Card>
)}
</div>
</div>
);
};
@@ -1,15 +1,14 @@
import { PaginationButtons } from "@/components/pagination-buttons";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import type { EmbeddedWalletUser } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
import { Flex, Switch } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { TWTable } from "components/shared/TWTable";
import { format } from "date-fns/format";
import Papa from "papaparse";
import { useCallback, useMemo, useState } from "react";
import { Button, Text } from "tw-components";
import { AddressCopyButton } from "tw-components/AddressCopyButton";
import { withinDays } from "utils/date-utils";
import { Analytics } from "./Analytics";
const ACTIVE_THRESHOLD_DAYS = 30;
@@ -26,13 +25,22 @@ const columns = [
columnHelper.accessor("ews_authed_user", {
header: "Email",
enableColumnFilter: true,
cell: (cell) => <Text>{cell.getValue()?.[0]?.email}</Text>,
cell: (cell) => (
<span className="text-sm">{cell.getValue()?.[0]?.email}</span>
),
}),
columnHelper.accessor("embedded_wallet", {
header: "Address",
cell: (cell) => {
const address = cell.getValue()?.[0]?.address;
return address ? <AddressCopyButton address={address} /> : null;
return address ? (
<CopyAddressButton
address={address}
copyIconPosition="left"
variant="ghost"
className="-translate-x-2"
/>
) : null;
},
}),
columnHelper.accessor("created_at", {
@@ -43,7 +51,11 @@ const columns = [
if (!value) {
return;
}
return <Text>{format(new Date(value), "MMM dd, yyyy")}</Text>;
return (
<span className="text-sm">
{format(new Date(value), "MMM dd, yyyy")}
</span>
);
},
}),
columnHelper.accessor("last_accessed_at", {
@@ -54,7 +66,11 @@ const columns = [
if (!value) {
return;
}
return <Text>{format(new Date(value), "MMM dd, yyyy")}</Text>;
return (
<span className="text-sm">
{format(new Date(value), "MMM dd, yyyy")}
</span>
);
},
}),
];
@@ -63,7 +79,6 @@ export const Users: React.FC<UsersProps> = ({
wallets,
isLoading,
isFetched,
trackingCategory,
}) => {
const [onlyActive, setOnlyActive] = useState(true);
@@ -119,11 +134,12 @@ export const Users: React.FC<UsersProps> = ({
}, [activePage, theWalletsWeWant]);
return (
<Flex flexDir="column" gap={10}>
<Flex flexDir="column" gap={6}>
<Flex dir="row" justify="space-between" align="center">
<div>
<div className="flex flex-col gap-6">
{/* Top section */}
<div className="flex justify-between items-center">
<Button
isDisabled={theWalletsWeWant.length === 0}
disabled={theWalletsWeWant.length === 0}
variant="outline"
onClick={downloadCSV}
size="sm"
@@ -131,15 +147,17 @@ export const Users: React.FC<UsersProps> = ({
Download as .csv
</Button>
<Flex gap={2} alignItems="center" justifyContent="flex-end">
<Text>Active last {ACTIVE_THRESHOLD_DAYS} days</Text>
<div className="flex gap-2 items-center justify-end">
<p className="text-sm text-muted-foreground">
Active last {ACTIVE_THRESHOLD_DAYS} days
</p>
<Switch
isChecked={onlyActive}
onChange={() => setOnlyActive(!onlyActive)}
checked={onlyActive}
onCheckedChange={(v) => setOnlyActive(v)}
disabled={wallets.length === 0}
/>
</Flex>
</Flex>
</div>
</div>
<TWTable
title="active in-app wallets"
@@ -156,9 +174,7 @@ export const Users: React.FC<UsersProps> = ({
totalPages={totalPages}
/>
)}
</Flex>
<Analytics trackingCategory={trackingCategory} />
</Flex>
</div>
</div>
);
};
@@ -1,13 +1,6 @@
import {
Box,
Icon,
Progress,
Tooltip,
VStack,
useColorModeValue,
} from "@chakra-ui/react";
import { FiHelpCircle } from "react-icons/fi";
import { Card, Heading, Text } from "tw-components";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { CircleHelpIcon } from "lucide-react";
import { toUSD } from "utils/number";
interface UsageCardProps {
@@ -19,16 +12,6 @@ interface UsageCardProps {
tooltip?: string;
}
const getProgressColor = (percent: number) => {
if (percent > 90) {
return "red";
}
if (percent > 50) {
return "yellow";
}
return "blue";
};
export const UsageCard: React.FC<UsageCardProps> = ({
name,
title,
@@ -37,75 +20,59 @@ export const UsageCard: React.FC<UsageCardProps> = ({
progress,
tooltip,
}) => {
const bg = useColorModeValue("backgroundCardHighlight", "transparent");
return (
<Card minH={36} p={6} bg={bg}>
<VStack gap={6} alignItems="flex-start">
<Heading as="h3" size="label.lg">
{name}
<div className="p-4 bg-background border border-border rounded-lg min-h-[140px] flex flex-col">
<div className="flex items-center gap-2">
<h3 className="text-base font-medium">{name}</h3>
{tooltip && (
<ToolTipLabel label={tooltip}>
<CircleHelpIcon className="text-muted-foreground size-4" />
</ToolTipLabel>
)}
</div>
{tooltip && (
<Tooltip
p={0}
bg="transparent"
boxShadow={"none"}
label={
<Card py={2} px={4} bgColor="backgroundHighlight">
<Text size="body.md">{tooltip}</Text>
</Card>
}
>
<Box as="span" display="relative">
<Icon
opacity={0.5}
as={FiHelpCircle}
position="absolute"
ml={1}
/>
</Box>
</Tooltip>
)}
</Heading>
<div className="h-6" />
<VStack gap={2} alignItems="flex-start" w="full">
{title && (
<Text size="body.md" mb={2}>
{title}
</Text>
)}
<div className="flex flex-col gap-2 mt-auto">
{title && <p className="mb-2 text-foreground font-semibold">{title}</p>}
{total !== undefined && (
<Text
size="body.md"
mb={2}
color={typeof total === "number" ? "bgBlack" : undefined}
>
{typeof total === "number" ? toUSD(total) : total}
</Text>
)}
{total !== undefined && (
<p className="text-muted-foreground text-sm">
{typeof total === "number" ? toUSD(total) : total}
</p>
)}
{progress !== undefined && (
<Progress
w="full"
size="xs"
value={progress}
rounded="full"
colorScheme={getProgressColor(progress)}
/>
)}
{progress !== undefined && <Progress value={progress} />}
{overage && (
<Text size="body.md" mt={2}>
Additional overage fees to your next invoice will be{" "}
<Text as="span" size="label.sm" color="bgBlack">
{toUSD(overage)}
</Text>
.
</Text>
)}
</VStack>
</VStack>
</Card>
{overage && (
<p className="text-muted-foreground mt-2 text-sm">
Additional overage fees to your next invoice will be{" "}
<span className="text-foreground">{toUSD(overage)}</span>
</p>
)}
</div>
</div>
);
};
function Progress(props: {
value: number;
}) {
return (
<div className="bg-muted rounded-full">
<div
className={cn(
"h-2 rounded-full",
props.value > 90
? "bg-red-600"
: props.value > 50
? "bg-yellow-600"
: "bg-blue-600",
)}
style={{
width: `${props.value}%`,
}}
/>
</div>
);
}
@@ -225,7 +225,7 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
{tableProps.isLoading && (
<div className="flex items-center justify-center">
<div className="flex py-4 gap-4 items-center">
<div className="flex py-4 gap-2 items-center">
<Spinner className="size-4" />
<p className="text-muted-foreground text-sm">
Loading {pluralize(tableProps.title, 0, false)}
@@ -1,17 +1,15 @@
import { Flex, type FlexProps, Icon } from "@chakra-ui/react";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import { SiReact } from "@react-icons/all-files/si/SiReact";
import { SiTypescript } from "@react-icons/all-files/si/SiTypescript";
import { SiUnity } from "@react-icons/all-files/si/SiUnity";
import type { IconType } from "react-icons/lib";
import { Text, TrackedLink } from "tw-components";
export function SupportedPlatformLink(props: {
platform: "React" | "React Native" | "Unity" | "TypeScript";
href: string;
noBorder?: boolean;
size: "sm" | "md";
trackingCategory: string;
bg?: FlexProps["bg"];
className?: string;
}) {
let icon: IconType = SiReact;
if (props.platform === "Unity") {
@@ -21,53 +19,18 @@ export function SupportedPlatformLink(props: {
}
return (
<TrackedLink
<TrackedLinkTW
category={props.trackingCategory}
label={`platform-${props.platform.replace(" ", "-").toLowerCase()}`}
href={props.href}
isExternal
_hover={{
textDecor: "none",
color: "blue.500",
}}
role="group"
target="_blank"
className={cn(
"flex items-center gap-2 text-muted-foreground hover:text-foreground text-sm",
props.className,
)}
>
<Flex
bg={props.bg}
gap={props.size === "sm" ? 2 : 3}
alignItems="center"
border={props.noBorder ? undefined : "1px solid"}
borderColor="borderColor"
px={props.noBorder ? 0 : 2}
py={1}
borderRadius="md"
_groupHover={{
borderColor: "blue.500",
}}
transition="border-color 200ms ease"
>
<Icon
as={icon}
w={props.size === "sm" ? 4 : 5}
h={props.size === "sm" ? 4 : 5}
color="faded"
_groupHover={{
color: "blue.500",
}}
transition="color 200ms ease"
/>
<Text
fontWeight={500}
color="heading"
_groupHover={{
color: "blue.500",
}}
fontSize={props.size === "sm" ? 12 : 14}
transition="color 200ms ease"
>
{props.platform}
</Text>
</Flex>
</TrackedLink>
{icon({ className: "size-4" })}
{props.platform}
</TrackedLinkTW>
);
}
+24 -21
View File
@@ -94,6 +94,21 @@ const GUIDES = [
];
const ConnectLanding: ThirdwebNextPage = () => {
const platforms = [
{
platform: "React",
href: "https://portal.thirdweb.com/typescript/v5/react/components/ConnectButton",
},
{
platform: "React Native",
href: "https://portal.thirdweb.com/react-native/latest/components/ConnectWallet",
},
{
platform: "Unity",
href: "https://portal.thirdweb.com/unity/wallets/prefab",
},
] as const;
return (
<LandingLayout
bgColor="#0F0F0F"
@@ -228,27 +243,15 @@ const ConnectLanding: ThirdwebNextPage = () => {
>
Supports
</Text>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
size="sm"
platform="React"
href="https://portal.thirdweb.com/typescript/v5/react/components/ConnectButton"
bg="#0E0E0E"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
size="sm"
platform="React Native"
href="https://portal.thirdweb.com/react-native/latest/components/ConnectWallet"
bg="#0E0E0E"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
size="sm"
platform="Unity"
href="https://portal.thirdweb.com/unity/wallets/prefab"
bg="#0E0E0E"
/>
{platforms.map(({ platform, href }) => (
<SupportedPlatformLink
key={platform}
trackingCategory={TRACKING_CATEGORY}
platform={platform}
href={href}
className="text-foreground hover:text-link-foreground bg-background px-3 py-2 rounded-lg text-xs border border-border hover:border-link-foreground"
/>
))}
</Flex>
<MiniPlayground trackingCategory={TRACKING_CATEGORY} />
@@ -1,19 +1,19 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { type ApiKey, useApiKeys } from "@3rdweb-sdk/react/hooks/useApi";
import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex, Grid, HStack, Icon, Spacer } from "@chakra-ui/react";
import { AppLayout } from "components/app-layouts/app";
import { EmbeddedWallets } from "components/embedded-wallets";
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys";
import { ConnectSidebar } from "core-ui/sidebar/connect";
import { ArrowRightIcon } from "lucide-react";
import { useRouter } from "next/router";
import { PageId } from "page-id";
import { useMemo, useState } from "react";
import { AiOutlineArrowRight } from "react-icons/ai";
import { Card, Heading, Text, TrackedLink } from "tw-components";
import type { ThirdwebNextPage } from "utils/types";
import { Analytics } from "../../../components/embedded-wallets/Users/Analytics";
import { SupportedPlatformLink } from "../../../components/wallets/SupportedPlatformLink";
const TRACKING_CATEGORY = "embedded-wallet";
@@ -64,44 +64,43 @@ const DashboardConnectEmbeddedWallets: ThirdwebNextPage = () => {
}
return (
<Flex flexDir="column" gap={8}>
<Flex flexDir="column" gap={2}>
<Flex
justifyContent="space-between"
direction={{ base: "column", lg: "row" }}
gap={4}
>
<Heading size="title.lg" as="h1">
<div>
<div className="flex lg:justify-between gap-4 flex-col lg:flex-row">
<div>
<h1 className="font-semibold text-3xl tracking-tigher">
In-App Wallets
</Heading>
{hasApiKeys && (
<HStack gap={3}>
{selectedKey && (
<ApiKeysMenu
apiKeys={apiKeys}
selectedKey={selectedKey}
onSelect={setSelectedKey}
/>
)}
</HStack>
)}
</Flex>
</h1>
<Text maxW="xl">
A wallet infrastructure that enables apps to create, manage, and
control their users wallets. Email login, social login, and
bring-your-own auth supported.{" "}
<TrackedLink
isExternal
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
label="learn-more"
category={TRACKING_CATEGORY}
color="primary.500"
>
Learn more
</TrackedLink>
</Text>
</Flex>
<div className="h-3" />
<p className="max-w-[500px] text-muted-foreground ">
A wallet infrastructure that enables apps to create, manage, and
control their users wallets. Email login, social login, and
bring-your-own auth supported.{" "}
<TrackedLinkTW
target="_blank"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
label="learn-more"
category={TRACKING_CATEGORY}
className="text-link-foreground hover:text-foreground"
>
Learn more
</TrackedLinkTW>
</p>
</div>
<div>
{hasApiKeys && selectedKey && (
<ApiKeysMenu
apiKeys={apiKeys}
selectedKey={selectedKey}
onSelect={setSelectedKey}
/>
)}
</div>
</div>
<div className="h-8" />
{keysQuery.isLoading ? (
<div className="flex h-[500px] items-center justify-center">
@@ -124,78 +123,62 @@ const DashboardConnectEmbeddedWallets: ThirdwebNextPage = () => {
</>
)}
<Spacer height={20} />
<div className="h-16" />
<Analytics trackingCategory={TRACKING_CATEGORY} />
<div className="h-5" />
<FooterSection />
</Flex>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<ViewDocs />
<Templates />
</div>
</div>
);
};
function FooterSection() {
return (
<Grid templateColumns={["1fr", "1fr 1fr"]} gap={5}>
<ViewDocs />
<Templates />
</Grid>
);
}
function ViewDocs() {
return (
<Card p={5} className="bg-muted/50">
<Flex gap={2} alignItems="center">
<Heading fontSize={16} as="h3">
View Docs
</Heading>
<Icon as={AiOutlineArrowRight} width={5} height={5} />
</Flex>
<div className="p-4 lg:p-6 bg-muted/50 border border-border rounded-lg">
<div className="flex items-center gap-2">
<h3 className="font-semibold">View Docs</h3>
<ArrowRightIcon className="size-4" />
</div>
<Spacer height={6} />
<div className="h-4" />
<Grid templateColumns={"1fr 1fr"} gap={3} maxW="400px">
<div className="grid grid-cols-2 gap-4">
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
size="md"
noBorder
platform="React"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
noBorder
size="md"
platform="Unity"
href="https://portal.thirdweb.com/unity/wallets/providers/embedded-wallet"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
noBorder
size="md"
platform="React Native"
href="https://portal.thirdweb.com/react-native/latest/connect/in-app-wallet"
href="https://portal.thirdweb.com/react/v5/in-app-wallet/get-started"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
noBorder
size="md"
platform="TypeScript"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
/>
</Grid>
</div>
<Spacer height={8} />
<div className="h-6" />
<Flex gap={2} alignItems="center">
<Heading fontSize={16} as="h3">
Relevant Guides
</Heading>
<Icon as={AiOutlineArrowRight} width={5} height={5} />
</Flex>
<div className="flex items-center gap-2">
<h3 className="font-semibold">Relevant Guides</h3>
<ArrowRightIcon className="size-4" />
</div>
<Spacer height={5} />
<div className="h-4" />
<Flex gap={3} flexDirection="column">
<div className="flex flex-col gap-3">
<GuideLink
href="https://blog.thirdweb.com/what-are-embedded-wallets/"
label="what-is-an-embedded-wallet"
@@ -230,8 +213,8 @@ function ViewDocs() {
>
Create a custom auth server
</GuideLink>
</Flex>
</Card>
</div>
</div>
);
}
@@ -241,7 +224,7 @@ function GuideLink(props: {
href: string;
}) {
return (
<TrackedLink
<TrackedLinkTW
category={TRACKING_CATEGORY}
label={"guide"}
trackingProps={{
@@ -249,26 +232,24 @@ function GuideLink(props: {
}}
href={props.href}
className="text-sm !text-muted-foreground hover:!text-foreground"
isExternal
target="_blank"
>
{props.children}
</TrackedLink>
</TrackedLinkTW>
);
}
function Templates() {
return (
<Card p={5} className="bg-muted/50">
<Flex gap={2} alignItems="center">
<Heading fontSize={16} as="h3">
Relevant Templates
</Heading>
<Icon as={AiOutlineArrowRight} width={5} height={5} />
</Flex>
<div className="p-4 lg:p-6 bg-muted/50 border border-border rounded-lg">
<div className="flex items-center gap-2">
<h3 className="font-semibold">Relevant Templates</h3>
<ArrowRightIcon className="size-4" />
</div>
<Spacer height={6} />
<div className="h-6" />
<Flex gap={3} flexDirection="column">
<div className="flex flex-col gap-3">
<GuideLink
href="https://github.com/thirdweb-example/embedded-smart-wallet"
label="embedded-smart-wallet"
@@ -296,8 +277,8 @@ function Templates() {
>
In-App Wallet With Custom UI [ReactNative]
</GuideLink>
</Flex>
</Card>
</div>
</div>
);
}