Skip to content
Merged
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
233 changes: 208 additions & 25 deletions apps/portal/src/components/others/AllSupportedWallets.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
"use client";

import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react";
import Image from "next/image";
import { useMemo, useState } from "react";
import {
getAllWalletsList,
getWalletInfo,
type WalletId,
} from "thirdweb/wallets";
import { DocLink, InlineCode } from "../Document";
import { DocLink } from "../Document/DocLink";
import { InlineCode } from "../Document/InlineCode";
import { Table, TBody, Td, Th, Tr } from "../Document/Table";
import { Button } from "../ui/button";
import { Input } from "../ui/input";

const specialWallets: {
[key in WalletId]?: boolean;
Expand All @@ -14,44 +26,215 @@ const specialWallets: {
smart: true,
};

export async function AllSupportedWallets() {
const wallets = await getAllWalletsList();
const ITEMS_PER_PAGE = 20;

const queryClient = new QueryClient();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Move QueryClient instantiation inside the component to avoid SSR issues.

Creating QueryClient at the module level can cause hydration mismatches and memory leaks in SSR environments. Initialize it inside the component or use a factory function.

Apply this diff:

-const queryClient = new QueryClient();
-
 export function AllSupportedWallets() {
+  const [queryClient] = useState(() => new QueryClient());
   return (

Also add the missing import:

-import { useMemo, useState } from "react";
+import { useMemo, useState } from "react";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/portal/src/components/others/AllSupportedWallets.tsx at line 31, move
the instantiation of QueryClient from the module level into the component
function to prevent SSR hydration mismatches and memory leaks. Also, add the
missing import statement for QueryClient from the appropriate library at the top
of the file.


export function AllSupportedWallets() {
return (
<QueryClientProvider client={queryClient}>
<AllSupportedWalletsContent />
</QueryClientProvider>
);
}
Comment on lines +33 to +39
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 explicit return type to the component function.

Per the coding guidelines, TypeScript functions should have explicit return types.

-export function AllSupportedWallets() {
+export function AllSupportedWallets(): JSX.Element {
📝 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
export function AllSupportedWallets() {
return (
<QueryClientProvider client={queryClient}>
<AllSupportedWalletsContent />
</QueryClientProvider>
);
}
export function AllSupportedWallets(): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<AllSupportedWalletsContent />
</QueryClientProvider>
);
}
🤖 Prompt for AI Agents
In apps/portal/src/components/others/AllSupportedWallets.tsx around lines 33 to
39, the AllSupportedWallets function component lacks an explicit return type.
Add the React.FC (or React.FunctionComponent) return type annotation to the
function declaration to comply with TypeScript coding guidelines for explicit
return types in components.


function AllSupportedWalletsContent() {
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 explicit return type to the component function.

-function AllSupportedWalletsContent() {
+function AllSupportedWalletsContent(): JSX.Element {
📝 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
function AllSupportedWalletsContent() {
-function AllSupportedWalletsContent() {
+function AllSupportedWalletsContent(): JSX.Element {
🤖 Prompt for AI Agents
In apps/portal/src/components/others/AllSupportedWallets.tsx at line 41, the
function AllSupportedWalletsContent lacks an explicit return type. Add the
appropriate React component return type, such as React.ReactElement or
JSX.Element, to the function signature to explicitly specify the return type of
the component.

const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);

const { data: wallets, isLoading: loading } = useQuery({
queryKey: ["allWalletsList"],
queryFn: async () => {
const allWallets = await getAllWalletsList();
return allWallets
.filter((w) => !(w.id in specialWallets))
.map((w) => ({
id: w.id,
name: w.name,
}));
},
staleTime: 1000 * 60 * 5, // 5 minutes
});

const filteredWallets = useMemo(() => {
if (!searchQuery) return wallets || [];
if (!wallets) return [];

setCurrentPage(1);
const query = searchQuery.toLowerCase();
return wallets.filter(
(wallet) =>
wallet.name.toLowerCase().includes(query) ||
wallet.id.toLowerCase().includes(query),
);
}, [wallets, searchQuery]);

Comment on lines +59 to +71
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove side effect from useMemo and fix inefficient re-renders.

The setCurrentPage(1) call inside useMemo is a side effect that violates React best practices and will cause the component to re-render unnecessarily on every search query change.

Apply this diff to fix the issue:

+  // Reset page when search query changes
+  useEffect(() => {
+    setCurrentPage(1);
+  }, [searchQuery]);
+
   const filteredWallets = useMemo(() => {
     if (!searchQuery) return wallets || [];
     if (!wallets) return [];
 
-    setCurrentPage(1);
     const query = searchQuery.toLowerCase();
     return wallets.filter(
       (wallet) =>
         wallet.name.toLowerCase().includes(query) ||
         wallet.id.toLowerCase().includes(query),
     );
-  }, [wallets, searchQuery]);
+  }, [wallets, searchQuery]);

Don't forget to import useEffect:

-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
🤖 Prompt for AI Agents
In apps/portal/src/components/others/AllSupportedWallets.tsx around lines 59 to
71, remove the side effect call setCurrentPage(1) from inside the useMemo hook
because useMemo should be pure and not cause side effects. Instead, move
setCurrentPage(1) into a separate useEffect hook that runs whenever searchQuery
changes. This will prevent unnecessary re-renders and adhere to React best
practices. Also, ensure useEffect is imported from React.

const totalPages = Math.ceil(filteredWallets.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentWallets = filteredWallets.slice(startIndex, endIndex);

const handlePreviousPage = () => {
setCurrentPage((prev) => Math.max(prev - 1, 1));
};

const handleNextPage = () => {
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
};

const handlePageClick = (page: number) => {
setCurrentPage(page);
};

if (loading) {
return (
<div className="flex items-center justify-center py-8">
<div className="text-muted-foreground">Loading wallets...</div>
</div>
);
}

return (
<Table>
<TBody>
<Tr>
<Th> Wallet </Th>
<Th> ID </Th>
</Tr>

{wallets
.filter((w) => !(w.id in specialWallets))
.map((w) => {
return (
<Tr key={w.id}>
<div className="space-y-6">
{/* Search Input */}
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="text"
placeholder="Search wallets by name or ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>

{/* Results count */}
<div className="text-sm text-muted-foreground">
{filteredWallets.length === wallets?.length
? `Showing ${filteredWallets.length} wallets`
: `Found ${filteredWallets.length} of ${wallets?.length} wallets`}
</div>

{/* Table */}
<Table>
<TBody>
<Tr>
<Th>Wallet</Th>
<Th>ID</Th>
</Tr>

{currentWallets.length === 0 ? (
<Tr>
<Td>
{searchQuery
? "No wallets found matching your search."
: "No wallets available."}
</Td>
</Tr>
) : (
currentWallets.map((wallet) => (
<Tr key={wallet.id}>
<Td>
<DocLink
className="flex flex-nowrap items-center gap-4 whitespace-nowrap"
href={`/wallets/external-wallets/${w.id}`}
href={`/wallets/external-wallets/${wallet.id}`}
>
<WalletImage id={w.id} />
{w.name}
<WalletImage id={wallet.id as WalletId} />
{wallet.name}
</DocLink>
</Td>
<Td>
<InlineCode code={`"${w.id}"`} />
<InlineCode code={`"${wallet.id}"`} />
</Td>
</Tr>
);
})}
</TBody>
</Table>
))
)}
</TBody>
</Table>

{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
{filteredWallets.length > 0 && (
<span className="ml-2">
(showing {startIndex + 1}-
{Math.min(endIndex, filteredWallets.length)} of{" "}
{filteredWallets.length})
</span>
)}
</div>

<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={handlePreviousPage}
disabled={currentPage === 1}
>
<ChevronLeftIcon className="size-4" />
Previous
</Button>

{/* Page numbers */}
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNumber: number;

if (totalPages <= 5) {
pageNumber = i + 1;
} else if (currentPage <= 3) {
pageNumber = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNumber = totalPages - 4 + i;
} else {
pageNumber = currentPage - 2 + i;
}

return (
<Button
key={pageNumber}
variant={currentPage === pageNumber ? "default" : "outline"}
size="sm"
onClick={() => handlePageClick(pageNumber)}
className="min-w-[32px]"
>
{pageNumber}
</Button>
);
})}
</div>

<Button
variant="outline"
size="sm"
onClick={handleNextPage}
disabled={currentPage === totalPages}
>
Next
<ChevronRightIcon className="size-4" />
</Button>
</div>
</div>
)}
</div>
);
}

async function WalletImage(props: { id: WalletId }) {
const img = await getWalletInfo(props.id, true);
function WalletImage(props: { id: WalletId }) {
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 explicit return type and consider adding error handling.

-function WalletImage(props: { id: WalletId }) {
+function WalletImage(props: { id: WalletId }): JSX.Element {

Also consider handling the error state from useQuery:

-  const { data: img } = useQuery({
+  const { data: img, error } = useQuery({
     queryKey: ["wallet-image", props.id],
     queryFn: () => getWalletInfo(props.id, true),
     staleTime: 1000 * 60 * 60 * 24, // 24 hours
   });

-  if (!img) {
+  if (!img || error) {
     return (
       <div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} />
     );
   }
📝 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
function WalletImage(props: { id: WalletId }) {
function WalletImage(props: { id: WalletId }): JSX.Element {
const { data: img, error } = useQuery({
queryKey: ["wallet-image", props.id],
queryFn: () => getWalletInfo(props.id, true),
staleTime: 1000 * 60 * 60 * 24, // 24 hours
});
if (!img || error) {
return (
<div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} />
);
}
// ...rest of rendering logic
}
🤖 Prompt for AI Agents
In apps/portal/src/components/others/AllSupportedWallets.tsx at line 228, the
WalletImage function lacks an explicit return type and does not handle potential
errors from the useQuery hook. Add an explicit return type to the WalletImage
function, such as React.ReactElement or JSX.Element, and include error handling
logic for the useQuery result, for example by checking for an error state and
rendering an appropriate fallback UI or message.

const { data: img } = useQuery({
queryKey: ["wallet-image", props.id],
queryFn: () => getWalletInfo(props.id, true),
staleTime: 1000 * 60 * 60 * 24, // 24 hours
});

if (!img) {
return (
<div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} />
);
}

return (
<Image alt="" className="rounded-lg" height={44} src={img} width={44} />
);
Expand Down
Loading