Skip to content
30 changes: 21 additions & 9 deletions apps/dashboard/src/@/hooks/useEmbeddedWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,31 @@ import { embeddedWalletsKeys } from "../query-keys/cache-keys";
const fetchAccountList = ({
jwt,
clientId,
ecosystemSlug,
pageNumber,
}: {
jwt: string;
clientId: string;
clientId?: string;
ecosystemSlug?: string;
pageNumber: number;
}) => {
return async () => {
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
url.searchParams.append("clientId", clientId);

// Add clientId or ecosystemSlug parameter
if (ecosystemSlug) {
url.searchParams.append("ecosystemSlug", ecosystemSlug);
} else if (clientId) {
url.searchParams.append("clientId", clientId);
}

url.searchParams.append("page", pageNumber.toString());

const res = await fetch(url.href, {
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
"x-client-id": clientId,
...(clientId && { "x-client-id": clientId }),
},
method: "GET",
});
Expand All @@ -38,23 +47,25 @@ const fetchAccountList = ({
};

export function useEmbeddedWallets(params: {
clientId: string;
clientId?: string;
ecosystemSlug?: string;
page: number;
authToken: string;
}) {
const { clientId, page, authToken } = params;
const { clientId, ecosystemSlug, page, authToken } = params;
const address = useActiveAccount()?.address;

return useQuery({
enabled: !!address && !!clientId,
enabled: !!address && !!(clientId || ecosystemSlug),
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId,
clientId || ecosystemSlug || "",
page,
),
});
Expand All @@ -67,7 +78,7 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
const address = useActiveAccount()?.address;

return useMutation({
mutationFn: async ({ clientId }: { clientId: string }) => {
mutationFn: async ({ clientId, ecosystemSlug }: { clientId?: string; ecosystemSlug?: string }) => {
const responses: WalletUser[] = [];
let page = 1;

Expand All @@ -77,12 +88,13 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
}>({
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId,
clientId || ecosystemSlug || "",
page,
),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export async function EcosystemLayoutSlug({
name: "Analytics",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
},
{
name: "Users",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/users`,
},
{
name: "Design (coming soon)",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/#`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { loginRedirect } from "@app/login/loginRedirect";
import { InAppWalletUsersPageContent } from "@app/team/[team_slug]/[project_slug]/(sidebar)/wallets/users/components";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";

Comment on lines +1 to +8
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add server-only import for server component.

This server component should import "server-only" at the top to clearly indicate it runs on the server edge.

+import "server-only";
 import { loginRedirect } from "@app/login/loginRedirect";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { loginRedirect } from "@app/login/loginRedirect";
import { InAppWalletUsersPageContent } from "@app/team/[team_slug]/[project_slug]/(sidebar)/wallets/users/components";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import "server-only";
import { loginRedirect } from "@app/login/loginRedirect";
import { InAppWalletUsersPageContent } from "@app/team/[team_slug]/[project_slug]/(sidebar)/wallets/users/components";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/users/page.tsx
at the top of the file (lines 1 to 8), add an import statement for "server-only"
to explicitly mark this component as a server component. Insert the line 'import
"server-only";' before any other imports to clearly indicate that this file runs
on the server edge.

export default async function EcosystemUsersPage(props: {
params: Promise<{ team_slug: string; slug: string }>;
}) {
const params = await props.params;
const [authToken, ecosystem, team] = await Promise.all([
getAuthToken(),
fetchEcosystem(params.slug, params.team_slug),
getTeamBySlug(params.team_slug),
]);

if (!authToken) {
loginRedirect(
`/team/${params.team_slug}/~/ecosystem/${params.slug}/users`,
);
}

if (!ecosystem || !team) {
redirect(`/team/${params.team_slug}`);
}

const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});

return (
<InAppWalletUsersPageContent
authToken={authToken}
client={client}
ecosystemSlug={ecosystem.slug}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

import type { SearchType } from "./types";

export function AdvancedSearchInput(props: {
onSearch: (searchType: SearchType, query: string) => void;
onClear: () => void;
isLoading: boolean;
hasResults: boolean;
}) {
const [searchType, setSearchType] = useState<SearchType>("email");
const [query, setQuery] = useState("");

const handleSearch = () => {
if (query.trim()) {
props.onSearch(searchType, query.trim());
}
};

const handleClear = () => {
setQuery("");
props.onClear();
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};

return (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Select
value={searchType}
onValueChange={(value) => setSearchType(value as SearchType)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix type safety issue in search type casting.

The type casting value as SearchType is not type-safe. The Select component should already ensure the value is valid, but consider adding runtime validation.

-          onValueChange={(value) => setSearchType(value as SearchType)}
+          onValueChange={(value) => {
+            if (value === "email" || value === "phone" || value === "id" || value === "address") {
+              setSearchType(value);
+            }
+          }}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/users/components/AdvancedSearchInput.tsx
at line 48, the current type casting of value to SearchType is unsafe. Replace
the direct cast with a runtime check to confirm the value is a valid SearchType
before calling setSearchType. This can be done by verifying the value against
the allowed SearchType values and only setting the state if valid, otherwise
handle the invalid case appropriately.

>
<SelectTrigger className="w-[140px] bg-card">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="id">ID</SelectItem>
<SelectItem value="address">Address</SelectItem>
</SelectContent>
</Select>

<div className="relative flex-1">
<Input
className="bg-card pl-9"
placeholder={`Search by ${searchType}...`}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
/>
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
</div>

<Button
onClick={handleSearch}
disabled={!query.trim() || props.isLoading}
size="sm"
>
{props.isLoading ? "Searching..." : "Search"}
</Button>
</div>

{props.hasResults && (
<div className="flex justify-center">
<Button
variant="outline"
size="sm"
onClick={handleClear}
className="gap-2"
>
Clear Search & Return to List
</Button>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { format } from "date-fns";
import type { ThirdwebClient } from "thirdweb";
import type { WalletUser } from "thirdweb/wallets";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";

const getUserIdentifier = (user: WalletUser) => {
const mainDetail = user.linkedAccounts[0]?.details;
return (
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address ??
mainDetail?.id ??
user.id
);
};

export function SearchResults(props: {
results: WalletUser[];
client: ThirdwebClient;
}) {
if (props.results.length === 0) {
return (
<Card>
<CardContent className="py-8">
<div className="flex flex-col items-center gap-3">
<p className="text-sm">No users found</p>
<p className="text-muted-foreground text-sm">
Try searching with different criteria
</p>
</div>
</CardContent>
</Card>
);
}

return (
<div className="space-y-4">
{props.results.map((user) => {
const walletAddress = user.wallets?.[0]?.address;
const createdAt = user.wallets?.[0]?.createdAt;
const mainDetail = user.linkedAccounts?.[0]?.details;
const email = mainDetail?.email as string | undefined;
const phone = mainDetail?.phone as string | undefined;

return (
<Card key={user.id}>
<CardHeader>
<CardTitle className="text-lg">User Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-muted-foreground">
User Identifier
</p>
<p className="text-sm">{getUserIdentifier(user)}</p>
</div>

{walletAddress && (
<div>
<p className="text-sm font-medium text-muted-foreground">
Wallet Address
</p>
<WalletAddress
address={walletAddress}
client={props.client}
/>
</div>
)}

{email && (
<div>
<p className="text-sm font-medium text-muted-foreground">
Email
</p>
<p className="text-sm">{email}</p>
</div>
)}

{phone && (
<div>
<p className="text-sm font-medium text-muted-foreground">
Phone
</p>
<p className="text-sm">{phone}</p>
</div>
)}

{createdAt && (
<div>
<p className="text-sm font-medium text-muted-foreground">
Created
</p>
<p className="text-sm">
{format(new Date(createdAt), "MMM dd, yyyy")}
</p>
</div>
)}

<div>
<p className="text-sm font-medium text-muted-foreground">
Login Methods
</p>
<div className="flex flex-wrap gap-1">
{user.linkedAccounts?.map((account, index) => (
<TooltipProvider
key={`${user.id}-${account.type}-${index}`}
>
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary" className="text-xs">
{account.type}
</Badge>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm space-y-1">
{Object.entries(account.details).map(
([key, value]) => (
<div key={key}>
<span className="font-medium">{key}:</span>{" "}
{String(value)}
</div>
),
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
);
}
Loading
Loading