Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-olives-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

expose WalletUser type for in app / ecosystem wallets
7 changes: 5 additions & 2 deletions apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ export const embeddedWalletsKeys = {
all: ["embeddedWallets"] as const,
wallet: (walletAddress: string) =>
[...embeddedWalletsKeys.all, walletAddress] as const,
embeddedWallets: (walletAddress: string, clientId: string | undefined) =>
[...embeddedWalletsKeys.wallet(walletAddress), clientId] as const,
embeddedWallets: (
walletAddress: string,
clientId: string | undefined,
page: number,
) => [...embeddedWalletsKeys.wallet(walletAddress), clientId, page] as const,
};

export const engineKeys = {
Expand Down
58 changes: 25 additions & 33 deletions apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,43 @@
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
import type { WalletUser } from "thirdweb/wallets";
import { embeddedWalletsKeys } from "../cache-keys";
import { useLoggedInUser } from "./useLoggedInUser";

// FIXME: Make API to return camelCase or transform
export type EmbeddedWalletUser = {
id: string;
client_id: string;
created_at: string;
last_accessed_at: string;
embedded_wallet?: {
id: string;
address: string;
chain: string;
wallet_user_id: string;
}[];
ews_authed_user: {
id: string;
authed_user_id: string;
email: string;
}[];
};

export function useEmbeddedWallets(clientId: string) {
export function useEmbeddedWallets(clientId: string, page: number) {
const { user, isLoggedIn } = useLoggedInUser();

return useQuery({
queryKey: embeddedWalletsKeys.embeddedWallets(
user?.address as string,
clientId as string,
clientId,
page,
),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user?.jwt}`,
},
},
const url = new URL(
`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`,
);
url.searchParams.append("clientId", clientId);
url.searchParams.append("page", page.toString());

const json = await res.json();
const res = await fetch(url.href, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user?.jwt}`,
},
});
if (!res.ok) {
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
}

return json.walletUsers as EmbeddedWalletUser[];
const json = await res.json();
return json as {
users: WalletUser[];
totalPages: number;
};
},
placeholderData: keepPreviousData,
enabled: !!user?.address && isLoggedIn && !!clientId,
});
}
161 changes: 83 additions & 78 deletions apps/dashboard/src/components/embedded-wallets/Users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,54 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { PaginationButtons } from "@/components/pagination-buttons";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
type EmbeddedWalletUser,
useEmbeddedWallets,
} from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
import { Spinner } 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 { withinDays } from "utils/date-utils";
import { useCallback, useState } from "react";
import type { WalletUser } from "thirdweb/wallets";

const ACTIVE_THRESHOLD_DAYS = 30;

const columnHelper = createColumnHelper<EmbeddedWalletUser>();
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
const mainDetail = accounts[0]?.details;
return (
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address ??
mainDetail?.id
);
};

const columnHelper = createColumnHelper<WalletUser>();
const columns = [
columnHelper.accessor("ews_authed_user", {
header: "Email",
columnHelper.accessor("linkedAccounts", {
header: "User Identifier",
enableColumnFilter: true,
cell: (cell) => (
<span className="text-sm">{cell.getValue()?.[0]?.email}</span>
),
cell: (cell) => {
const identifier = getUserIdentifier(cell.getValue());
return <span className="text-sm">{identifier}</span>;
},
id: "user_identifier",
}),
columnHelper.accessor("embedded_wallet", {
columnHelper.accessor("wallets", {
header: "Address",
cell: (cell) => {
const address = cell.getValue()?.[0]?.address;
const address = cell.getValue()[0]?.address;
return address ? <WalletAddress address={address} /> : null;
},
id: "address",
}),
columnHelper.accessor("created_at", {
columnHelper.accessor("wallets", {
header: "Created",
cell: (cell) => {
const value = cell.getValue();
const value = cell.getValue()[0]?.createdAt;

if (!value) {
return;
Expand All @@ -48,59 +61,64 @@ const columns = [
</span>
);
},
id: "created_at",
}),
columnHelper.accessor("last_accessed_at", {
header: "Last login",
columnHelper.accessor("linkedAccounts", {
header: "Login Methods",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

siiick

cell: (cell) => {
const value = cell.getValue();

if (!value) {
return;
}
const loginMethodsDisplay = value.reduce((acc, curr) => {
if (acc.length === 2) {
acc.push("...");
}
if (acc.length < 2) {
acc.push(curr.type);
}
return acc;
}, [] as string[]);
const loginMethods = value.map((v) => v.type).join(", ");
return (
<span className="text-sm">
{format(new Date(value), "MMM dd, yyyy")}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-sm">
{loginMethodsDisplay.join(", ")}
</TooltipTrigger>
<TooltipContent>
<span className="text-sm">{loginMethods}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
id: "login_methods",
}),
];

export const InAppWalletUsersPageContent = (props: {
clientId: string;
trackingCategory: string;
}) => {
const [onlyActive, setOnlyActive] = useState(true);
const walletsQuery = useEmbeddedWallets(props.clientId);
const wallets = walletsQuery?.data || [];

const activeWallets = useMemo(() => {
if (!wallets) {
return [];
}

return wallets.filter((w) => {
const lastAccessedAt = w.last_accessed_at;
return (
lastAccessedAt && withinDays(lastAccessedAt, ACTIVE_THRESHOLD_DAYS)
);
});
}, [wallets]);

const theWalletsWeWant = useMemo(() => {
return (onlyActive ? activeWallets : wallets) ?? [];
}, [activeWallets, onlyActive, wallets]);
const [activePage, setActivePage] = useState(1);
const walletsQuery = useEmbeddedWallets(props.clientId, activePage);
const { users: wallets, totalPages } = walletsQuery?.data || {
users: [],
totalPages: 1,
};

// TODO: Make the download CSV grab all data instead of merely what's on the current page
const downloadCSV = useCallback(() => {
if (theWalletsWeWant.length === 0) {
if (wallets.length === 0) {
return;
}
const csv = Papa.unparse(
theWalletsWeWant.map((row) => ({
email: row.ews_authed_user[0]?.email,
address: row.embedded_wallet?.[0]?.address || "",
created: format(new Date(row.created_at), "MMM dd, yyyy"),
last_login: format(new Date(row.last_accessed_at), "MMM dd, yyyy"),
wallets.map((row) => ({
user_identifier: getUserIdentifier(row.linkedAccounts),
address: row.wallets[0]?.address || "",
created: format(
new Date(row.wallets[0]?.createdAt ?? ""),
"MMM dd, yyyy",
),
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
})),
);
const csvUrl = URL.createObjectURL(
Expand All @@ -110,28 +128,15 @@ export const InAppWalletUsersPageContent = (props: {
tempLink.href = csvUrl;
tempLink.setAttribute("download", "download.csv");
tempLink.click();
}, [theWalletsWeWant]);

const [activePage, setActivePage] = useState(1);
const itemsPerPage = 20;
const totalPages =
theWalletsWeWant.length <= itemsPerPage
? 1
: Math.ceil(theWalletsWeWant.length / itemsPerPage);

const itemsToShow = useMemo(() => {
const startIndex = (activePage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return theWalletsWeWant.slice(startIndex, endIndex);
}, [activePage, theWalletsWeWant]);
}, [wallets]);

return (
<div>
<div className="flex flex-col gap-6">
{/* Top section */}
<div className="flex items-center justify-between">
<Button
disabled={theWalletsWeWant.length === 0}
disabled={wallets.length === 0}
variant="outline"
onClick={downloadCSV}
size="sm"
Expand All @@ -140,20 +145,20 @@ export const InAppWalletUsersPageContent = (props: {
</Button>

<div className="flex items-center justify-end gap-2">
<p className="text-muted-foreground text-sm">
Active last {ACTIVE_THRESHOLD_DAYS} days
</p>
<Switch
checked={onlyActive}
onCheckedChange={(v) => setOnlyActive(v)}
disabled={wallets.length === 0}
/>
{walletsQuery.isPlaceholderData && (
<>
<Spinner className="size-4" />
<p className="text-muted-foreground text-sm">
Loading page {activePage} of {totalPages}
</p>
</>
)}
</div>
</div>

<TWTable
title="active in-app wallets"
data={itemsToShow}
title="in-app wallets"
data={wallets}
columns={columns}
isPending={walletsQuery.isPending}
isFetched={walletsQuery.isFetched}
Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/src/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const THIRDWEB_API_HOST = "/api/server-proxy/api";
export const THIRDWEB_ANALYTICS_API_HOST = "/api/server-proxy/analytics";

export const THIRDWEB_EWS_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST || "https://ews.thirdweb.com";
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST ||
"https://in-app-wallet.thirdweb.com";

export const THIRDWEB_PAY_DOMAIN =
process.env.NEXT_PUBLIC_PAY_URL || "pay.thirdweb-dev.com";
Expand Down
8 changes: 0 additions & 8 deletions apps/dashboard/src/utils/date-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { differenceInDays } from "date-fns/differenceInDays";
import { format } from "date-fns/format";
import { isValid } from "date-fns/isValid";
import { parseISO } from "date-fns/parseISO";

const DATE_TIME_LOCAL_FORMAT = "yyyy-MM-dd HH:mm";

Expand All @@ -18,12 +16,6 @@ export function toDateTimeLocal(date?: Date | number | string) {
: undefined;
}

export function withinDays(dateISO: string, days: number) {
const date = parseISO(dateISO);
const today = new Date();
return differenceInDays(today, date) <= days;
}

export function remainingDays(isoDate: string) {
const currentDate = new Date();
const targetDate = new Date(isoDate);
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type {
} from "../wallets/smart/types.js";

export type {
WalletUser,
InAppWalletAuth,
/**
* @deprecated use InAppWalletAuth instead
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/wallets/in-app/core/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MultiStepAuthArgsType,
SingleStepAuthArgsType,
} from "../authentication/types.js";
import type { UserStatus } from "./enclave-wallet.js";

export type Ecosystem = {
id: EcosystemWalletId;
Expand All @@ -33,6 +34,8 @@ export type InAppWalletAutoConnectOptions = {
chain?: Chain;
};

export type WalletUser = UserStatus;

export type InAppWalletSocialAuth = SocialAuthOption;
export type InAppWalletOAuth = OAuthOption;
export type InAppWalletAuth = AuthOption;
Expand Down
Loading