-
Notifications
You must be signed in to change notification settings - Fork 619
unified project layout #7888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
unified project layout #7888
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
WalkthroughReplaces many legacy team/project layouts and pages with a new ProjectPage wrapper, removes PostHog feature-flag helper and webhook-config APIs/UI, consolidates Payments/Engine/Contracts/Tokens/Wallets/RPC/Transactions pages into header-driven ProjectPage patterns, and adds several new header/footer and action components. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as ProjectPage Header
participant Action as ActionLink / LinkGroup
participant Router as Next Router
User->>UI: Click header action (external/docs/playground or custom component)
UI->>Action: render inline button or dropdown
alt action is Link
Action->>Router: navigate/open link (internal or external)
else action is Component (e.g., Import button)
Action->>User: open dialog component
end
sequenceDiagram
autonumber
actor User
participant Page as Engine Page (ProjectPage)
participant Dialog as ImportEngineButton
participant API as Dashboard API (/v1/teams/:teamSlug/engine)
participant Router as next/navigation
User->>Page: Click "Import Engine"
Page->>Dialog: open dialog
User->>Dialog: submit {name, url}
Dialog->>Dialog: validate (zod)
alt valid
Dialog->>API: POST /v1/teams/:teamSlug/engine {name, normalizedUrl}
alt success
API-->>Dialog: 200 { id }
Dialog->>Router: navigate /team/:team/:project/engine/dedicated
Dialog-->>User: show success toast
else error
API-->>Dialog: error
Dialog-->>User: show error toast
end
else invalid
Dialog-->>User: show validation errors
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
53bba64 to
1fbe286
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7888 +/- ##
==========================================
- Coverage 56.54% 56.53% -0.01%
==========================================
Files 904 904
Lines 58592 58592
Branches 4140 4143 +3
==========================================
- Hits 33131 33126 -5
- Misses 25355 25360 +5
Partials 106 106
🚀 New features to boost your workflow:
|
size-limit report 📦
|
.../(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/explorer/components/scalar.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (12)
apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx (1)
89-91: Fix typo in user-facing label: “Chain Infrastucture” → “Chain Infrastructure”.Visible label typo; should be corrected.
Apply this diff:
- label: "Chain Infrastucture", + label: "Chain Infrastructure",apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx (3)
286-293: Duplicate push of storage service results in two entriesserviceToAdd is pushed twice when serviceMeta.name === "storage", creating duplicate entries in services. Push exactly once after mutating actions.
Apply this fix:
- // add the actions changes to the base service - if (serviceMeta.name === "storage") { - serviceToAdd.actions = serviceMeta.actions as ("read" | "write")[]; - services.push(serviceToAdd); - } - - services.push(serviceToAdd); + // add the actions changes to the base service (single push) + if (serviceMeta.name === "storage") { + serviceToAdd.actions = serviceMeta.actions as ("read" | "write")[]; + } + services.push(serviceToAdd);
174-197: Saving with no file clears the existing project imageIf file is undefined, the code still calls updateProjectClient with image: undefined, which can unintentionally remove the current image. Early return when no file is provided.
Apply this fix:
updateProjectImage={async (file) => { - let uri: string | undefined; - - if (file) { - // upload to IPFS - uri = await upload({ - client: props.client, - files: [file], - }); - } - - await updateProjectClient( + if (!file) { + return; // No change; avoid clearing existing image + } + // upload to IPFS + const uri = await upload({ + client: props.client, + files: [file], + }); + + await updateProjectClient( { projectId: props.project.id, teamId: props.project.teamId, }, { image: uri, }, ); router.refresh(); }}
952-954: Fix user-facing grammar in deletion warningMinor but visible text: “will not able to” → “will not be able to”.
- const description = - "The associated Client ID and Secret Key will not able to access thirdweb services after deletion. This action is irreversible"; + const description = + "The associated Client ID and Secret Key will not be able to access thirdweb services after deletion. This action is irreversible";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/webhooks/components/webhooks.client.tsx (1)
112-113: Normalize createdAt before using date-fnsThe Universal Bridge client returns
createdAtas a string (ISO), butformatDistanceToNowexpects aDateor numeric timestamp. Passing a string will yield invalid dates at runtime.Please update:
• File:
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/webhooks/components/webhooks.client.tsx
• Lines ~112–113Diff:
- {formatDistanceToNow(webhook.createdAt, { addSuffix: true })} + {formatDistanceToNow(new Date(webhook.createdAt), { addSuffix: true })}Alternatively, you can normalize dates when fetching:
const data = await getWebhooks(...); return data.map(w => ({ ...w, createdAt: new Date(w.createdAt) }));apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/your-factories.tsx (1)
41-50: Critical: Secret Key Leakage Risk viaclientThirdwebClientThe
getClientThirdwebClienthelper (apps/dashboard/src/@/constants/thirdweb-client.client.ts) passes your JWT (secretKey) into aThirdwebClientinstance. That server-authenticated client is then sent into the browser via theclientThirdwebClientprop in:
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/your-factories.tsx
• Line 44:<AsyncYourFactories clientThirdwebClient={props.clientThirdwebClient} … />
• Line 92:<FactoryContracts client={props.clientThirdwebClient} … />This exposes your JWT/secretKey to end users.
Action items:
- Replace any usage of the server-authenticated client in client components with a publishable client (no
secretKey) instantiated on the client side (e.g. using a public API key).- Route all privileged operations (writes, sensitive reads) through Next.js server actions or internal API routes that use your authenticated server client.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx (1)
137-161: “Refresh Balance” likely doesn’t invalidate the underlying queriesuseWalletBalance (from thirdweb/react) manages its own query keys. Invalidating ["walletBalance", selectedChainId] probably won’t match, so the button may do nothing. Easiest robust fix: re-mount balance cells via a refresh nonce.
Apply this diff to implement a refresh nonce and key the balance cells:
@@ const [selectedChainId, setSelectedChainId] = useState<number>(1); const [showSmartAccount, setShowSmartAccount] = useState(false); const queryClient = useQueryClient(); + const [refreshNonce, setRefreshNonce] = useState(0); @@ - <Button - className="z-20 h-auto p-1.5 [&[data-pending='true']_svg]:animate-spin" - onClick={async (e) => { - const buttonEl = e.currentTarget; - buttonEl.setAttribute("data-pending", "true"); - await queryClient.invalidateQueries({ - queryKey: ["walletBalance", selectedChainId], - }); - - buttonEl.setAttribute("data-pending", "false"); - }} - size="sm" - variant="ghost" - > + <Button + className="z-20 h-auto p-1.5 [&[data-pending='true']_svg]:animate-spin" + onClick={(e) => { + const buttonEl = e.currentTarget; + buttonEl.setAttribute("data-pending", "true"); + setRefreshNonce((n) => n + 1); + // brief visual feedback + setTimeout(() => buttonEl.setAttribute("data-pending", "false"), 400); + }} + size="sm" + variant="ghost" + > <RefreshCcwIcon className="size-4" /> </Button> @@ - <ServerWalletTableRow + <ServerWalletTableRow key={wallet.id} wallet={wallet} project={project} teamSlug={teamSlug} client={client} chainId={selectedChainId} showSmartAccount={showSmartAccount} + refreshNonce={refreshNonce} /> @@ -function ServerWalletTableRow(props: { +function ServerWalletTableRow(props: { wallet: Wallet; project: Project; teamSlug: string; client: ThirdwebClient; chainId: number; showSmartAccount: boolean; + refreshNonce: number; }) { - const { wallet, project, teamSlug, client, chainId, showSmartAccount } = + const { wallet, project, teamSlug, client, chainId, showSmartAccount, refreshNonce } = props; @@ - <WalletBalanceCell + <WalletBalanceCell + key={`smart-${props.wallet.id}-${refreshNonce}`} address={smartAccountAddressQuery.data} chainId={chainId} client={client} /> @@ - <WalletBalanceCell + <WalletBalanceCell + key={`wallet-${wallet.id}-${refreshNonce}`} address={wallet.address} chainId={chainId} client={client} />Alternative: if you prefer to stay with Query invalidation, expose and re-use the exact queryKey from useWalletBalance (e.g., via a helper or documented key factory) and invalidate by predicate. Without that guarantee, re-mounting is safer.
Also applies to: 170-178, 269-276, 277-279, 342-347, 354-359
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx (2)
26-28: Fix loginRedirect return path (missing project slug)The redirect-after-login path omits project_slug and likely points to a non-existent route. It should mirror the current page path.
- if (!authToken) { - loginRedirect(`/team/${team_slug}/wallets/settings`); - } + if (!authToken) { + loginRedirect(`/team/${team_slug}/${project_slug}/settings/wallets`); + }If loginRedirect doesn’t throw (i.e., doesn’t call next/navigation.redirect internally), add a return to prevent subsequent code from executing.
38-41: Prevent leaking JWT to the browser viagetClientThirdwebClientThe helper in
thirdweb-client.client.tssimply forwards yourjwtassecretKeyinto aThirdwebClientinstance, with no stripping or safe serialization. Passing this client into a client component embeds your token in the browser.Action items:
- Remove passing
client(with JWT) into the client component.- Move all privileged Thirdweb calls into server components, server actions, or internal API routes that read the JWT via
getAuthToken().- In client components, use a lightweight wrapper (e.g.
fetchJson) that hits your own/api/*endpoints, which proxy to the Thirdweb SDK using the server-side token.Locations needing fixes:
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx @ lines 37–41
• Any other pages or components passinggetClientThirdwebClient({ jwt, … })into client-side codePlease refactor to keep your JWT strictly server-side.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/PaymentLinksTable.client.tsx (1)
288-321: Fix destructive action labels: says “Delete Webhook” but deletes a payment linkThe dialog and primary button copy are inconsistent with the action. Update to “payment link” for clarity and correctness.
- <DialogDescription> - This action cannot be undone. This will permanently delete the - payment. - </DialogDescription> + <DialogDescription> + This action cannot be undone. This will permanently delete the + payment link. + </DialogDescription> @@ - Delete Webhook + Delete Payment Linkapps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (2)
36-38: Add an early return after loginRedirect to halt executionIn server components, ensure the function returns immediately after calling loginRedirect to prevent any subsequent logic from running.
Apply this diff:
if (!authToken) { - loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`); + return loginRedirect( + `/team/${params.team_slug}/${params.project_slug}/tokens`, + ); }
48-51: Prevent importing a client-only helper into a Server ComponentThe
getClientThirdwebClienthelper lives inapps/dashboard/src/@/constants/thirdweb-client.client.ts, which Next.js will treat as a client module (due to the.client.tsextension). Importing a client module into a server component (page.tsx) will break the build.Please refactor as follows:
- Rename
thirdweb-client.client.tsto either:
thirdweb-client.server.ts- or drop the suffix entirely (
thirdweb-client.ts)- Update all imports of
getClientThirdwebClientaccordingly.- Optionally, if you need a client‐side variant later, keep that in a separate
*.client.tshelper; keep your server code in the.server.tsfile.This will ensure your page remains a pure Server Component without accidentally pulling in client-only code.
🧹 Nitpick comments (77)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/settings/SponsorshipPolicies/index.tsx (2)
186-188: Align copy: “Sponsorship policies” vs “Sponsorship rules”Elsewhere on this page we surface “Sponsorship rules” in notifications. Consider standardizing the terminology in the header or the notifications to avoid user-facing inconsistency.
Apply this if you prefer “rules” in the header:
- <h2 className="text-xl font-semibold tracking-tight mb-0.5"> - Sponsorship policies - </h2> + <h2 className="text-xl font-semibold tracking-tight mb-0.5"> + Sponsorship rules + </h2>Alternatively, keep “policies” here and update the notification copy (outside this hunk) to match.
191-199: Prefer design tokens over hardcoded color scalePer the dashboard guidelines, favor tokens like text-primary to avoid binding to a specific shade and improve theme compatibility.
- <UnderlineLink - className="text-primary-500" + <UnderlineLink + className="text-primary" href="https://portal.thirdweb.com/wallets/smart-wallet/sponsorship-rules" rel="noopener noreferrer" target="_blank" >If you adopt this, consider updating any other UnderlineLink instances in this file for consistency.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-table/tx-table-ui.tsx (2)
88-100: Hide decorative icon from screen readers for better a11y.The ArrowLeftRightIcon here is purely visual; mark it aria-hidden to avoid noisy announcements.
- <div className="p-4 rounded-full bg-background border border-border"> - <ArrowLeftRightIcon className="size-5 text-muted-foreground" /> - </div> + <div className="p-4 rounded-full bg-background border border-border"> + <ArrowLeftRightIcon + aria-hidden="true" + focusable="false" + className="size-5 text-muted-foreground" + /> + </div>
255-257: Add an accessible name to the status filter trigger.The Select lacks a visible label; providing an aria-label improves screen reader UX without changing visuals.
- <SelectTrigger className="min-w-[160px] rounded-full"> + <SelectTrigger + aria-label="Filter transactions by status" + className="min-w-[160px] rounded-full" + >apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx (1)
1-4: Add explicit return type and use ReactNode type import per guidelines.The coding guidelines ask for explicit return types and shared/type imports instead of
React.ReactNodeinline. Minimal tweak below.Apply this diff:
"use client"; +import type { ReactNode } from "react"; import { AtomIcon, BookTextIcon, @@ -export function TeamSidebarLayout(props: { +export function TeamSidebarLayout(props: { layoutPath: string; - children: React.ReactNode; + children: ReactNode; ecosystems: Array<{ name: string; slug: string }>; chainSubscriptions: Array<{ chainId: number; chainName: string; slug: string; }>; -}) { +}): JSX.Element {Also applies to: 17-27
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx (2)
59-65: Data-driven gating for Engine is solid; failure degrades gracefullyUsing the presence of engine instances to gate the UI is the right call. On fetch failure,
hasEngineInstancesbecomes false which safely hides the menu without exploding the UI.If you want clearer intent, mildly prefer Boolean() over double-negation for readability:
-const hasEngineInstances = !!engineInstances.data?.length; +const hasEngineInstances = Boolean(engineInstances.data?.length);
1-1: Mark this layout as server-onlyPer dashboard guidelines, server components should start with the server-only directive to prevent accidental client bundling.
Apply:
+import "server-only"; import { redirect } from "next/navigation";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (1)
22-27: Add explicit return type and extract props to a named aliasMatches the repo’s TypeScript guidelines (explicit return types, prefer type aliases). Improves readability and reuse.
Apply:
-export function ProjectSidebarLayout(props: { - layoutPath: string; - children: React.ReactNode; - hasEngineInstances: boolean; -}) { +type ProjectSidebarLayoutProps = { + layoutPath: string; + children: React.ReactNode; + hasEngineInstances: boolean; +}; + +export function ProjectSidebarLayout( + props: ProjectSidebarLayoutProps, +): React.JSX.Element {apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx (4)
786-789: Use NavLink for internal navigation (design system guideline)For internal routes, prefer NavLink to get built-in active states and consistent semantics across the dashboard. Keep Button asChild if desired.
474-478: Disable Save until a new avatar is selectedPrevents accidental “save” with no file (and complements the early return above).
saveButton={{ - disabled: false, + disabled: !projectAvatar, isPending: updateProjectAvatarMutation.isPending, onClick: handleSave, }}
161-164: Fix typos in comment (pagae → page) and punctuationNon-functional, improves readability.
- // Can't open new project in new team or new team landing pagae because it takes a while for the transfer and it doesn't show up in new team immediately - // so the safe option is to just redirect to the current team landing page + // Can't open new project in new team or new team landing page because it takes a while for the transfer and it doesn't show up in the new team immediately, + // so the safe option is to just redirect to the current team landing page.
738-742: Remove redundant getServiceByName callserviceDefinition already has the data; the second lookup and truthy check are unnecessary.
- const serviceName = getServiceByName( - serviceDefinition.name as ServiceName, - ); - const shouldShow = !hidden && serviceName; + const shouldShow = !hidden;apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/webhooks/components/webhooks.client.tsx (7)
65-73: Set React Query staleTime ≥ 60s per dashboard guidelinesAdd a minimal cache window to avoid unnecessary refetches on tab focus or minor state changes.
Apply this diff:
const webhooksQuery = useQuery({ queryFn: async () => { return await getWebhooks({ clientId: props.clientId, teamId: props.teamId, }); }, queryKey: ["webhooks", props.clientId, props.teamId], + staleTime: 60_000, });
183-186: Use exact matching and full key on invalidation (create) to avoid over-invalidationCurrent invalidation prefixes on clientId and will invalidate all teams for that client. Prefer exact match on the same key used by the query.
Apply this diff:
- return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], - }); + return queryClient.invalidateQueries({ + queryKey: ["webhooks", props.clientId, props.teamId], + exact: true, + });
330-334: Use exact matching and full key on invalidation (delete) for consistencyMirror the query key and avoid broad invalidations across other teams.
Apply this diff:
- return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], - }); + return queryClient.invalidateQueries({ + queryKey: ["webhooks", props.clientId, props.teamId], + exact: true, + });
362-364: Avoid duplicate invalidation on deleteYou invalidate in the mutation’s onSuccess and again in the per-call onSuccess. One is enough; keep the mutation-level one.
Apply this diff:
- return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], - });
99-101: Nit: Use “URL” consistentlyColumn header reads “Url” while the form label uses “URL”. Align for consistency.
Apply this diff:
- <TableHead>Url</TableHead> + <TableHead>URL</TableHead>
203-212: Simplify post-success form handlingform.reset() plus clearErrors() already covers most of the manual setValue calls. The extra sets are redundant.
Apply this diff:
toast.success("Webhook created successfully"); form.reset(); form.clearErrors(); - form.setValue("url", ""); - form.setValue("label", ""); - form.setValue("version", "2"); - form.setValue("secret", undefined);
64-64: Add explicit return types to components (TS guideline)Aligns with the project rule for explicit return types in TSX files.
Apply this diff:
-export function PayWebhooksPage(props: PayWebhooksPageProps) { +export function PayWebhooksPage(props: PayWebhooksPageProps): JSX.Element {-function CreateWebhookButton(props: PropsWithChildren<PayWebhooksPageProps>) { +function CreateWebhookButton(props: PropsWithChildren<PayWebhooksPageProps>): JSX.Element {-function DeleteWebhookButton( +function DeleteWebhookButton( props: PropsWithChildren<PayWebhooksPageProps & { webhookId: string }>, -) { +): JSX.Element {Also applies to: 153-153, 316-316
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx (4)
1-1: Use dashboard UI primitives, not workspace buttonIn apps/dashboard, per guidelines, import UI components from "@/components/ui/*". Mixing sources can lead to inconsistent styling/bundling.
Apply:
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
1-4: Mark server component explicitlyThis is a server page. Add the server-only marker to prevent accidental client usage and to catch invalid imports at build time.
Apply:
+import "server-only"; import { Button } from "@workspace/ui/components/button"; import { ArrowLeftIcon, CircleAlertIcon } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation";
9-9: Fix import path/casing for UnderlineLinkThe import path uses a PascalCase filename. In this repo, the component is typically exported as "underline-link" (kebab-case). Case mismatches will break on case-sensitive filesystems.
Proposed change:
-import { UnderlineLink } from "@/components/ui/UnderlineLink"; +import { UnderlineLink } from "@/components/ui/underline-link";If this component doesn’t exist under dashboard’s "@/components/ui", consider adding a local export or re-export instead of reaching into portal.
51-55: Add accessible label to icon-only back buttonThe back button is icon-only; add an aria-label for a11y.
- <Button variant="outline" size="icon" className="rounded-full" asChild> - <Link href={`/team/${team_slug}/${project_slug}/settings`}> + <Button variant="outline" size="icon" className="rounded-full" asChild> + <Link + href={`/team/${team_slug}/${project_slug}/settings`} + aria-label="Back to project settings" + > <ArrowLeftIcon className="size-4" /> </Link> </Button>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/your-factories.tsx (5)
1-1: Mark server component explicitlyThis module runs server-only logic (serverThirdwebClient, contract metadata). Add the server-only marker.
+import "server-only"; import { PlusIcon } from "lucide-react";
11-11: Verify FactoryContracts import pathEnsure the "./factory-contracts" path is correct and stable. A previous version used a different casing/path. Broken relative imports will only surface at runtime.
13-21: Public API looks good; consider extracting a props typeExported function signature is fine. Extracting a named Props type improves reuse and discoverability across the unified layout.
Example:
export type YourFactoriesSectionProps = { teamId: string; projectId: string; authToken: string; teamSlug: string; projectSlug: string; clientThirdwebClient: ThirdwebClient; }; export function YourFactoriesSection(props: YourFactoriesSectionProps) { ... }
81-81: Prefer includes over indexOf for readabilityMinor readability improvement.
- return m.name.indexOf("AccountFactory") > -1 ? c : null; + return m.name.includes("AccountFactory") ? c : null;
70-87: Consider bounding async concurrency when scanning contractsFetching compiler metadata for all deployments in parallel can spike load for large projects. Use a concurrency limit to smooth server load.
I can provide a p-map-based refactor if desired. Example sketch:
import pMap from "p-map"; const factories = ( await pMap( deployedContracts, async (c) => { /* same logic */ }, { concurrency: 5 }, ) ).filter(Boolean);apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/CreateWebhookModal.tsx (1)
231-237: Wire Dialog onOpenChange to preserve accessibility (Esc/overlay close) and avoid double open handlersCurrently the Dialog is controlled via
open={isOpen}, but there’s noonOpenChange, and the Trigger button also callshandleOpenModal. This prevents overlay/Escape from closing the dialog and can double-fire open logic. Suggest controlling viaonOpenChangeand removing the button onClick.Apply this diff:
- <Dialog open={isOpen}> + <Dialog + open={isOpen} + onOpenChange={(v) => { + if (v) { + handleOpenModal(); + } else { + handleCloseModal(); + } + }} + > <DialogTrigger asChild> - <Button className="gap-1.5 rounded-full" onClick={handleOpenModal}> + <Button className="gap-1.5 rounded-full"> <PlusIcon className="size-4" /> Create Webhook </Button> </DialogTrigger>apps/dashboard/src/@/components/contracts/import-contract/modal.tsx (1)
138-146: Harden fetch error handling (check res.ok and invalid JSON)The code assumes a valid JSON body. If the metadata service returns a non-2xx or bad JSON, the error becomes opaque. Add
res.okguard and JSON parse safety.- const res = await fetch( - `https://contract.thirdweb.com/metadata/${chainId}/${contractAddress}`, - ); - const json = await res.json(); + const res = await fetch( + `https://contract.thirdweb.com/metadata/${chainId}/${contractAddress}`, + ); + if (!res.ok) { + throw new Error(`Failed to fetch metadata (${res.status} ${res.statusText})`); + } + const json = await res.json().catch(() => { + throw new Error("Invalid JSON response from metadata service"); + });apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PayAnalytics.tsx (2)
74-75: Empty state now returns null; consider minimal placeholder to keep layout stableReturning
nullremoves the prior FTUX and can lead to a visually empty section. If intentional, fine. Otherwise, render a slim placeholder card with a short “No data in selected range” message to avoid layout jump.
1-1: Mark as a server component explicitlyThis file does server-only data fetching. Add the server directive to prevent accidental client imports.
+import "server-only"; import { ResponsiveSuspense } from "responsive-rsc";apps/dashboard/src/@/components/analytics/responsive-time-filters.tsx (1)
25-25: Re-check responsive behavior; current classes force horizontal, left-aligned layout on mobilePreviously this stacked on small screens and right-aligned on large. If that behavior is still desired, restore responsive direction/justification. If the unified layout intentionally left-aligns filters, ignore this.
Apply if you want to keep the prior responsive UX:
- <div className="flex gap-3 flex-row"> + <div className="flex flex-col lg:flex-row lg:justify-end gap-3">Note: flex-row is default; it can be omitted unless overridden elsewhere.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx (1)
82-82: Nit: simplify padding classes to reduce redundancyUsing both p-4 and py-5 sets vertical padding twice. Consider making it explicit by axis to avoid overrides.
Here’s a minimal tweak:
- <div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5"> + <div className="flex flex-col lg:flex-row lg:justify-between px-4 lg:px-6 py-5 lg:items-center gap-5">apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/PaymentLinksTable.client.tsx (1)
116-133: Clarify empty state copy to match “Payment Links” contextText currently says “payments” which can be confusing in this section. Recommend aligning copy with “payment links” to set correct expectations.
- title="No payments configured yet" - description="Create a payment to receive any token in seconds." + title="No payment links configured yet" + description="Create a payment link to receive any token in seconds." @@ - <Button className="gap-1" variant="default" size="sm"> + <Button className="gap-1" variant="default" size="sm"> <PlusIcon className="size-4" /> - Create Payment + Create Payment Link </Button>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/users/page.tsx (1)
12-13: Comment doesn’t match behavior; clarify the redirect targetThe code redirects to the wallets root, not a distinct “users” tab. Update the comment for accuracy.
Apply this diff:
- // Default to the users tab - redirect(`/team/${params.team_slug}/${params.project_slug}/wallets`); + // Redirect to the canonical wallets page (users content is the default) + redirect(`/team/${params.team_slug}/${params.project_slug}/wallets`);apps/dashboard/src/@/api/analytics.ts (2)
662-688: Leftover internal webhook summary cache appears unused; remove or re-expose a wrapperThis private _cached_getWebhookSummary isn’t exported and likely became dead code after decommissioning webhook analytics. Keeping stale caches increases maintenance burden.
Consider removing this block:
-const _cached_getWebhookSummary = unstable_cache( - async ( - params: AnalyticsQueryParams & { webhookId: string }, - authToken: string, - ): Promise<{ data: WebhookSummaryStats[] } | { error: string }> => { - const searchParams = buildSearchParams(params); - searchParams.append("webhookId", params.webhookId); - - const res = await fetchAnalytics({ - authToken, - url: `v2/webhook/summary?${searchParams.toString()}`, - init: { - method: "GET", - }, - }); - if (!res.ok) { - const reason = await res.text(); - return { error: reason }; - } - - return (await res.json()) as { data: WebhookSummaryStats[] }; - }, - ["getWebhookSummary"], - { - revalidate: 60 * 60, // 1 hour - }, -);If you keep it, re-introduce an exported wrapper (e.g., getWebhookSummary) or move it to a module that still needs it.
Additionally (outside this hunk): if you remove this, also drop the WebhookSummaryStats import at the top to prevent unused imports.
690-718: Remove or export the webhook latency cache
The private_cached_getWebhookLatencyis dead code—there’s nogetWebhookLatencyexport to invoke it. You have two options:
- Remove the cache and drop
WebhookLatencyStatsfrom your imports.- Restore an exported wrapper, for example:
export function getWebhookLatency( params: AnalyticsQueryParams & { webhookId?: string }, authToken: string, ) { return _cached_getWebhookLatency(normalizedParams(params), authToken); }apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/payments/page.tsx (2)
1-3: Use dashboard app UI primitives from the local aliasPer our dashboard guidelines, import Button from "@/components/ui/button" rather than the workspace package to keep bundle boundaries and theming consistent.
Apply:
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
19-23: Minor: consider gating downstream work behind auth for efficiencyFetching team and project in parallel with getAuthToken is fine, but if authToken is missing you’ll redirect anyway. A small optimization is to await the token first and early-exit to avoid wasted work.
Example:
-const [team, project, authToken] = await Promise.all([...]); -if (!authToken) { loginRedirect(...); } +const authToken = await getAuthToken(); +if (!authToken) { loginRedirect(...); } +const [team, project] = await Promise.all([ + getTeamBySlug(team_slug), + getProject(team_slug, project_slug), +]);apps/dashboard/src/@/components/ui/tabs.tsx (1)
227-252: Add an explicit return type to TabPathLinks and consider centralizing active-state via NavLink
- Add a return type to satisfy our explicit typing guideline.
- Optional: We duplicate active-state logic here; our guideline prefers NavLink for internal routes.
Apply the return type:
-export function TabPathLinks(props: { +export function TabPathLinks(props: { links: TabPathLink[]; className?: string; tabContainerClassName?: string; shadowColor?: string; scrollableClassName?: string; bottomLineClassName?: string; -}) { +}): JSX.Element {If desired later, we can explore swapping the underlying Link for NavLink in TabLinks to remove custom pathname matching.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (2)
1-2: Use dashboard UI primitives from "@/components/ui/*"Per dashboard guidelines, prefer importing UI from "@/components/ui/*" instead of workspace-level packages.
Apply this diff:
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
25-27: Add explicit return types to server functionsTypeScript guideline: add explicit return types. This also helps TS flow control around redirects.
Apply these diffs:
export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string }>; -}) { +}): Promise<JSX.Element> {-async function AssetsPageAsync(props: { +async function AssetsPageAsync(props: { teamId: string; projectId: string; authToken: string; client: ThirdwebClient; teamSlug: string; projectSlug: string; -}) { +}): Promise<JSX.Element> {Also applies to: 116-123
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/FeatureCard.client.tsx (2)
69-71: Use design tokens instead of raw Tailwind colorsReplace bg-violet-500 with a tokenized color (e.g., bg-accent) to stay within the design system.
- <span className="bg-violet-500 size-1.5 rounded-full" /> + <span className="bg-accent size-1.5 rounded-full" />
16-18: Remove unused color propcolor is declared but not used. Drop it until there’s a concrete use-case, or wire it into the UI with tokenized classes.
- color?: "green" | "violet";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx (2)
3-4: Prefer dashboard UI import pathAlign Button import with dashboard convention.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
56-62: Trim unused props from Cards or put them to useCards accepts client, teamId, and projectId but doesn’t use them. Either remove them to keep the API minimal, or apply them where needed. Removing is preferable for now.
Apply this diff here:
-export function Cards(props: { - teamSlug: string; - projectSlug: string; - client: ThirdwebClient; - teamId: string; - projectId: string; -}) { +export function Cards(props: { teamSlug: string; projectSlug: string }) {And update the call site in apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx accordingly:
<Cards teamSlug={params.team_slug} projectSlug={params.project_slug} />apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx (1)
1-1: Use dashboard Button importSwap workspace UI import for the app’s UI primitive path.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "../../ui/button";If you prefer alias imports (recommended), use:
- import { Button } from "@/components/ui/button";
apps/dashboard/src/@/hooks/useEngine.ts (1)
14-16: Decouple hooks from page-level components by moving shared types to a stable moduleImporting types from deep page component paths couples this hook to the app routing layer and makes refactors brittle. Prefer re-exporting or relocating
ResultItemandEngineStatusto a shared types barrel (e.g.,@/types/engineor a localtypes.ts) and import from there.Example (conceptual, assuming a new shared types module is created):
-import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes"; -import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table"; +import type { ResultItem, EngineStatus } from "@/types/engine";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/page.tsx (1)
4-5: Add server-only guard to this server pageTo prevent accidental client-only module imports and enforce RSC boundaries, add the server-only directive.
Add at the very top of the file:
import "server-only";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/page.tsx (1)
5-5: Add server-only directive to enforce RSC boundariesThis page does server-side data fetching and should explicitly mark itself server-only to avoid accidental client imports.
Add at the top:
import "server-only";apps/dashboard/src/@/components/blocks/project-page/project-page-footer.tsx (2)
8-14: Add explicit return type for the exported functionPer repo TS guidelines, annotate return types on exported functions.
-export function ProjectPageFooter(props: ProjectPageFooterProps) { +export function ProjectPageFooter(props: ProjectPageFooterProps): JSX.Element { return ( <footer className="container"> <FooterLinksSection {...props} /> </footer> ); }
1-4: Consider relocating FooterLinksSection to a shared UI module to avoid deep app-route couplingThis shared block depends on a deeply nested app route component via a long relative path. Moving
FooterLinksSection(or a slim wrapper for it) under@/components/...and importing from there would improve layering and maintainability.apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/payments/page.tsx (2)
1-1: Addserver-onlydirective for server component safetyThis page is a server component and handles JWTs. Add the server-only assertion at the very top.
+import "server-only"; import { notFound } from "next/navigation";
8-13: Annotate explicit return type for the page functionOur TS guidelines prefer explicit return types. Since this is async and returns JSX, annotate
Promise<JSX.Element>.-export default async function Page(props: { +export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string; }>; -}) { +}): Promise<JSX.Element> {apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/overview/engine-instances-table.tsx (1)
878-885: Disable destructive submit until “reason” is selectedThe schema requires a reason; we can give immediate UX feedback by disabling the submit until a reason is chosen.
- <Button - className="gap-2" - disabled={ - !form.watch("confirmDeletion") || - deleteCloudHostedEngine.isPending - } + <Button + className="gap-2" + disabled={ + !form.watch("confirmDeletion") || + deleteCloudHostedEngine.isPending || + !form.watch("reason") + } type="submit" variant="destructive" >apps/dashboard/src/@/components/blocks/project-page/project-page.tsx (2)
26-34: Handle empty tabs gracefullyAn empty
tabsarray currently renders an empty Tab bar instead of the Separator. Checklengthbefore rendering.- {props.tabs ? ( + {props.tabs?.length ? ( <TabPathLinks className="-mt-8" tabContainerClassName="container" links={props.tabs} /> ) : ( <Separator /> )}
22-24: Add explicit return typePer our TS guidelines, annotate the component’s return type.
-export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) { +export function ProjectPage( + props: React.PropsWithChildren<ProjectPageProps>, +): JSX.Element { return ( … ); }Also applies to: 43-44
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/import-contract-button.tsx (1)
9-16: Optional: extract props type and annotate return for clarityDefine a named props type and annotate the return for consistency with our TS rules.
-export function ImportContractButton(props: { - teamId: string; - projectId: string; - projectSlug: string; - teamSlug: string; - client: ThirdwebClient; -}) { +type ImportContractButtonProps = { + teamId: string; + projectId: string; + projectSlug: string; + teamSlug: string; + client: ThirdwebClient; +}; + +export function ImportContractButton( + props: ImportContractButtonProps, +): JSX.Element {Also applies to: 18-31, 33-42
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx (2)
1-1: Addserver-onlyfor this redirector pageThis is a server-only redirector; add the directive at the top.
+import "server-only"; import { redirect } from "next/navigation";
3-6: Optional: use conventionalPageexport name for consistencyAcross routes, using
export default async function Page()improves consistency. Not required by Next.js, but helpful for uniformity.-export default async function WebhooksPage(props: { +export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string }>; }) {apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/page.tsx (3)
26-30: Safely build the redirect URL with URLSearchParamsDirectly interpolating
searchParams.importUrlinto the query string risks malformed URLs if it contains?,&, or#. Use URLSearchParams to properly encode the value.- redirect( - `/team/${params.team_slug}/${params.project_slug}/engine/dedicated/import?importUrl=${searchParams.importUrl}`, - ); + const qs = new URLSearchParams({ importUrl: searchParams.importUrl }).toString(); + redirect( + `/team/${params.team_slug}/${params.project_slug}/engine/dedicated/import?${qs}`, + );
71-76: Unreachable prefill: redirect makes prefillImportUrl always undefinedBecause the early redirect (Line 26) triggers whenever
importUrlexists,prefillImportUrl={searchParams.importUrl}will never receive a value on this page. Decide one of:
- Keep redirect for legacy deep links, and drop
prefillImportUrlhere.- Remove the redirect and rely on the header’s Import dialog with prefill.
Option A – drop prefill:
- <ImportEngineButton - prefillImportUrl={searchParams.importUrl} - teamSlug={params.team_slug} - projectSlug={params.project_slug} - /> + <ImportEngineButton + teamSlug={params.team_slug} + projectSlug={params.project_slug} + />Option B – remove redirect (use dialog prefill):
- if (searchParams.importUrl) { - redirect( - `/team/${params.team_slug}/${params.project_slug}/engine/dedicated/import?${qs}`, - ); - } + // Keep legacy route logic removed; dialog handles prefill via `searchParams.importUrl`.Also applies to: 26-30
52-55: Surface engine fetch errors to users
res.erroris silently ignored; the UI always renders an empty list. Consider showing a lightweight error state so users know the call failed.- <EngineInstancesList - instances={res.data || []} - projectSlug={params.project_slug} - team={team} - /> + {res.error ? ( + <div className="container text-sm text-red-500"> + Failed to load Engine instances. Please try again. + </div> + ) : ( + <EngineInstancesList + instances={res.data || []} + projectSlug={params.project_slug} + team={team} + /> + )}Also applies to: 81-85
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx (2)
86-91: Avoid duplicate literals: reuse defaultRange variableYou define
const defaultRange: DurationId = "last-30";but pass the string literal again togetFiltersFromSearchParams. Reuse the variable for consistency/maintainability.- const { range, interval } = getFiltersFromSearchParams({ - defaultRange: "last-30", + const { range, interval } = getFiltersFromSearchParams({ + defaultRange, from: searchParams.from, interval: searchParams.interval, to: searchParams.to, });
548-569: VerifyHeaderusage and cleanup
It appears theHeaderfunction exported from
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx
isn’t imported or referenced elsewhere in the repo. Please confirm and then:• If it’s only used by the default page component in this file, drop the
exportkeyword so it’s a private helper.
• If it’s intended for reuse, move it into a sharedcomponents/folder and update the import paths.
• Otherwise, delete the entireHeaderdefinition to remove dead code.Let me know once you’ve verified its usage.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/import/import-engine-dialog.tsx (3)
38-41: Enforce HTTPS for imported Engine URLsIf you intend to only allow secure Engine endpoints, refine the schema to require
https://explicitly.-const formSchema = z.object({ - name: z.string().min(1, "Name is required"), - url: z.string().url("Please enter a valid URL").min(1, "URL is required"), -}); +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + url: z + .string() + .url("Please enter a valid URL") + .refine((u) => { + try { + return new URL(u).protocol === "https:"; + } catch { + return false; + } + }, "URL must start with https://"), +});
52-67: Type-safety and error shape guard around apiServerProxy
res.errorassumes a specific shape. IfapiServerProxyreturns a different error layout, this may throw a confusing error. Consider typingapiServerProxyresponse here and guardingres.errorexistence before using it.- const res = await apiServerProxy({ + const res: { ok: boolean; error?: string } = await apiServerProxy({ body: JSON.stringify({ name: data.name, url, }), headers: { "Content-Type": "application/json", }, method: "POST", pathname: `/v1/teams/${teamIdOrSlug}/engine`, }); if (!res.ok) { - throw new Error(res.error); + throw new Error(res.error || "Unknown error"); }
83-91: Nit: improve UX by disabling form during mutationPrevent accidental double-submits and indicate busy state by disabling inputs and the submit button when pending.
- const importMutation = useMutation({ + const importMutation = useMutation({ mutationFn: async (importParams: ImportEngineParams) => { await importEngine({ ...importParams, teamIdOrSlug: props.teamSlug }); router.push( `/team/${props.teamSlug}/${props.projectSlug}/engine/dedicated`, ); }, }); @@ - <form onSubmit={form.handleSubmit(onSubmit)}> + <form onSubmit={form.handleSubmit(onSubmit)}> <Dialog> <DialogTrigger asChild> - <Button className="gap-1.5 rounded-full" variant="outline"> + <Button + className="gap-1.5 rounded-full" + variant="outline" + disabled={importMutation.isPending} + > <ImportIcon className="size-4" /> Import Engine </Button> </DialogTrigger> @@ - <Button className="min-w-28 gap-1.5 rounded-full" type="submit"> + <Button + className="min-w-28 gap-1.5 rounded-full" + type="submit" + disabled={importMutation.isPending} + aria-busy={importMutation.isPending} + > {importMutation.isPending ? ( <Spinner className="size-4" /> ) : ( <DownloadIcon className="size-4" /> )} Import </Button>Also applies to: 107-116, 184-193
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx (2)
114-133: Prefer design system tokens over hard-coded Tailwind colorsThe CTA uses hard-coded colors (bg-green-600). Prefer design tokens (e.g., bg-primary text-primary-foreground hover:bg-primary/90) to maintain theming consistency across the dashboard.
Apply this minimal diff to align with tokens:
- <a - className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm" + <a + className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 font-medium text-sm text-primary-foreground transition-all hover:bg-primary/90 hover:shadow-sm" href="https://portal.thirdweb.com/insight" rel="noopener noreferrer" target="_blank" >
6-6: Mark this page as server-only to prevent accidental client bundlingThese pages execute server-side logic (auth, data fetching). Explicitly marking the file server-only reduces risk of accidental client usage/imports.
Add this at the very top of the file:
import "server-only";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx (1)
12-12: Mark page as server-onlyThis page runs auth and data fetching on the server. Add the directive to avoid accidental client bundling/imports.
Insert at the very top:
import "server-only";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx (5)
1-2: Mark this file as a Server Component with server-onlyThis page runs on the server and imports server-only APIs (auth token, redirects). Add the server-only guard to prevent accidental client bundling/imports.
+import "server-only"; import { Button } from "@workspace/ui/components/button"; import { PlusIcon } from "lucide-react";
1-1: Prefer dashboard UI primitives from "@/components/ui/button"Dashboard apps should import UI primitives from "@/components/ui/*" per guidelines. If this path is available/aliased in this repo, align the import; otherwise, ignore.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
29-39: Short-circuit unauthenticated requests and ensure redirect flow terminates
- Avoid calling getProject when not authenticated.
- Ensure loginRedirect returns/throws to stop execution (most wrappers call next/navigation.redirect internally, which throws).
Refactor to check auth before fetching the project and explicitly return for clarity.
- const [params, authToken] = await Promise.all([props.params, getAuthToken()]); - - const project = await getProject(params.team_slug, params.project_slug); - - if (!authToken) { - loginRedirect(`/team/${params.team_slug}/${params.project_slug}/payments`); - } + const params = await props.params; + const authToken = await getAuthToken(); + + if (!authToken) { + // loginRedirect should never return; return here for clarity in types and control flow + return loginRedirect( + `/team/${params.team_slug}/${params.project_slug}/payments`, + ); + } + + const project = await getProject(params.team_slug, params.project_slug); if (!project) { redirect(`/team/${params.team_slug}`); }If loginRedirect does not return never, please confirm and we can adjust the pattern across pages to avoid fall-through after redirects.
43-49: Narrow interval type to match PayAnalytics prop contractPayAnalytics expects interval: "day" | "week". Ensure the filter util returns a narrowed union type (preferred), or locally assert while wiring props to avoid widening to string.
Preferred: update the util’s return type in apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/time.ts so interval is typed as "day" | "week".
As a minimal, local patch here:
<PayAnalytics client={client} - interval={interval} + interval={interval as "day" | "week"} projectClientId={project.publishableKey} projectId={project.id} range={range} teamId={project.teamId} authToken={authToken} />I can also open a follow-up to tighten the time.ts signature to avoid these casts.
Also applies to: 139-147
86-93: Nit: pluralize “UI Component”Minor copy nit for consistency with the rest of the playground and docs (“UI Components”).
{ href: "https://playground.thirdweb.com/payments/ui-components", - label: "UI Component", + label: "UI Components", },
apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx
Show resolved
Hide resolved
apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx
Outdated
Show resolved
Hide resolved
...)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/import/import-engine-dialog.tsx
Show resolved
Hide resolved
...p/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/FeatureCard.client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
Show resolved
Hide resolved
1fbe286 to
50e4064
Compare
50e4064 to
2d4cbd5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (1)
42-58: Action Required: Correct Payments and Insights Routes
The Payments link currently uses
href={${props.layoutPath}/payments}
but the actual route file lives at
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/payments/page.tsx.
Update thehrefto
${props.layoutPath}/webhooks/payments.There is no
insightorinsightspage under the(sidebar)directory.
Either:
- Rename or add the route at
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insights/page.tsx
(orinsight/page.tsx), or- Remove/disable the Insights menu entry if it’s not yet implemented.
Locations to update:
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (lines 42–58, and corresponding Insights section around lines 63–80 and 85–118)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx (1)
23-25: Return after loginRedirect to stop execution in server componentEnsure no further code runs after initiating an auth redirect.
if (!authToken) { loginRedirect(`/team/${params.team_slug}/${params.project_slug}/contracts`); + return null; }apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx (1)
27-29: Return after loginRedirect to halt server executionPrevent accidental rendering when the user is unauthenticated.
if (!authToken) { loginRedirect(`/team/${params.team_slug}/${params.project_slug}/insight`); + return null; }apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (1)
36-38: Return after loginRedirect to prevent further server executionif (!authToken) { loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`); + return null; }
♻️ Duplicate comments (5)
apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx (1)
39-49: External links correctly hardened with rel="noopener noreferrer"Good addition of rel on external links and the Action wrapper.
Also applies to: 135-142, 153-160
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (4)
22-22: Guard against invalid page query param to avoid NaN and negative pagesNumber.parseInt can return NaN (e.g., page=foo). That would propagate into pagination and the EOAs request. Default to page 1 when parsing fails and clamp to >= 1.
Apply:
- const pageSize = 10; - const currentPage = Number.parseInt(searchParams.page ?? "1"); + const pageSize = 10; + const parsed = Number.parseInt(searchParams.page ?? "1", 10); + const currentPage = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;Also applies to: 58-60
111-121: Do not pass server-created ThirdwebClient or managementAccessToken into a client component (EngineChecklist)EngineChecklist is a client component. Passing client (with JWT) and managementAccessToken across the RSC boundary risks secret leakage and serialization failures.
Apply:
<EngineChecklist isManagedVault={isManagedVault} - client={client} hasTransactions={hasTransactions} - managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} testTxWithWallet={searchParams.testTxWithWallet as string | undefined} wallets={wallets ?? []} />Follow-up (outside this file):
- Update apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx to construct a browser-safe client internally (e.g., getClientThirdwebClient()).
- Do not pass managementAccessToken to the browser. Use server actions or an internal API route to perform privileged operations (e.g., creating server wallets), returning only non-sensitive results.
130-138: Don’t forward a JWT-backed client through server → client component pathsTransactionsAnalyticsPageContent currently accepts client and forwards it to children (e.g., TransactionsTable), which are likely client components. Drop the client prop entirely and let client-side components instantiate a safe client locally or call server actions/internal APIs.
Apply:
<TransactionsAnalyticsPageContent - client={client} project={project} searchParams={searchParams} showAnalytics={hasTransactions && !searchParams.testTxWithWallet} teamSlug={params.team_slug} wallets={wallets} />Follow-up (outside this file):
- Remove client from TransactionsAnalyticsPageContent props and stop forwarding it to TransactionsTable.
- Ensure any "use client" children create their own client (no JWT) or talk to server actions.
141-154: Avoid sending secrets to the browser; clamp totalPages ≥ 1; don’t render raw error messages
- Remove client and managementAccessToken from ServerWalletsTable call; the UI layer should not receive secrets or a server-configured client. Use server actions/internal APIs.
- totalPages can be 0 when no records; many pagers expect ≥ 1. Clamp to at least 1.
- Rendering eoas.error.message can leak internal details. Show a friendly message and log the detailed error on the server.
Apply:
{eoas.error ? ( - <div>Error: {eoas.error.message}</div> + <div>Failed to load Server Wallets. Please try again later.</div> ) : ( <ServerWalletsTable - client={client} currentPage={currentPage} - managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} - totalPages={Math.ceil(eoas.data.totalRecords / pageSize)} + totalPages={Math.max(1, Math.ceil(eoas.data.totalRecords / pageSize))} totalRecords={eoas.data.totalRecords} wallets={eoas.data.items as Wallet[]} /> )}Follow-up (outside this file):
- Update ServerWalletsTable and wallet-table-ui.client.tsx to drop client and managementAccessToken from props. Instantiate a browser-safe client inside the client component and route privileged actions through server actions/internal API endpoints.
🧹 Nitpick comments (9)
apps/dashboard/src/@/hooks/useEngine.ts (1)
14-15: Centralize ResultItem and EngineStatus in a Shared BarrelI’ve confirmed that both
ResultItemandEngineStatusare correctly exported from their respective component files, so you can safely decoupleuseEngine.tsfrom deep route paths:Tasks:
Create a shared types barrel at
apps/dashboard/src/@/types/engine.ts:// apps/dashboard/src/@/types/engine.ts export type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes"; export type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table";Update the import in
apps/dashboard/src/@/hooks/useEngine.ts:-import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes"; -import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table"; +import type { ResultItem, EngineStatus } from "@/types/engine";This change reduces coupling to your route structure and guards against accidental breakages if those components move in the future.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (2)
22-26: Add an explicit props type alias and return type for the component.Aligns with the code guidelines: explicit function return types and preferring type aliases. Improves reusability and IDE help.
Apply this diff to the function signature:
-export function ProjectSidebarLayout(props: { - layoutPath: string; - children: React.ReactNode; - hasEngineInstances: boolean; -}) { +export function ProjectSidebarLayout( + props: ProjectSidebarLayoutProps, +): JSX.Element {Add this type alias near the imports (outside the selected lines):
export type ProjectSidebarLayoutProps = { layoutPath: string; hasEngineInstances: boolean; children: React.ReactNode; };
28-121: Optional: memoize link arrays to stabilize referential equality and avoid unnecessary renders.FullWidthSidebarLayout likely diff-checks links; memoizing prevents re-renders when props don’t change.
Apply this scoped refactor:
@@ -"use client"; +"use client"; +import { useMemo } from "react"; @@ - return ( - <FullWidthSidebarLayout - contentSidebarLinks={[ + const contentSidebarLinks = useMemo( + () => [ { exactMatch: true, href: props.layoutPath, icon: HomeIcon, label: "Overview", }, { separator: true, }, { group: "Build", links: [ { href: `${props.layoutPath}/wallets`, icon: WalletProductIcon, label: "Wallets", }, { href: `${props.layoutPath}/transactions`, icon: ArrowLeftRightIcon, label: "Transactions", }, { href: `${props.layoutPath}/contracts`, icon: ContractIcon, label: "Contracts", }, ], }, { separator: true, }, { group: "Monetize", links: [ { href: `${props.layoutPath}/payments`, icon: PayIcon, label: "Payments", }, { href: `${props.layoutPath}/tokens`, icon: TokenIcon, label: ( <span className="flex items-center gap-2"> Tokens <Badge>New</Badge> </span> ), }, ], }, { separator: true, }, { group: "Scale", links: [ { href: `${props.layoutPath}/insight`, icon: InsightIcon, label: "Insight", }, { href: `${props.layoutPath}/account-abstraction`, icon: SmartAccountIcon, label: "Account Abstraction", }, { href: `${props.layoutPath}/rpc`, icon: RssIcon, label: "RPC", }, { href: `${props.layoutPath}/vault`, icon: LockIcon, label: "Vault", }, ...(props.hasEngineInstances ? [ { href: `${props.layoutPath}/engine`, icon: DatabaseIcon, label: ( <span className="flex items-center gap-2">Engine</span> ), }, ] : []), ], }, - ]} - footerSidebarLinks={[ + ], + [props.layoutPath, props.hasEngineInstances], + ); + + const footerSidebarLinks = useMemo( + () => [ { href: `${props.layoutPath}/webhooks/contracts`, icon: WebhookIcon, isActive: (pathname) => { return pathname.startsWith(`${props.layoutPath}/webhooks`); }, label: "Webhooks", }, { href: `${props.layoutPath}/settings`, icon: SettingsIcon, label: "Project Settings", }, { separator: true, }, { href: "https://portal.thirdweb.com", icon: BookTextIcon, label: "Documentation", }, { href: "https://playground.thirdweb.com/connect/sign-in/button", icon: JoystickIcon, label: "Playground", }, - ]} - > + ], + [props.layoutPath], + ); + + return ( + <FullWidthSidebarLayout + contentSidebarLinks={contentSidebarLinks} + footerSidebarLinks={footerSidebarLinks} + > @@ - </FullWidthSidebarLayout> + </FullWidthSidebarLayout>Also applies to: 121-149
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/RecentPaymentsSection.client.tsx (2)
39-41: Strengthen React Query cache keyInclude teamId (and optionally limit/offset) in the queryKey to avoid accidental cache collisions if those props vary across instances.
- queryKey: ["recent-payments", props.projectClientId], + queryKey: ["recent-payments", props.projectClientId, props.teamId, 10, 0],
30-41: Set staleTime to align with refetch interval (avoid unnecessary re-renders)You’re polling every 10s but leaving staleTime at the default (0). Set staleTime to match the refetch cadence to reduce churn.
>({ queryFn: async () => { const res = await getPayments({ clientId: props.projectClientId, limit: 10, offset: 0, teamId: props.teamId, }); return res; }, queryKey: ["recent-payments", props.projectClientId], + staleTime: 10_000, refetchInterval: 10_000, });apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (1)
1-1: Use dashboard UI primitives for consistencyIn dashboard apps, prefer "@/components/ui/button" over workspace-level Button to keep styling/behavior consistent.
- import { Button } from "@workspace/ui/components/button"; + import { Button } from "@/components/ui/button";apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx (1)
1-1: Prefer dashboard UI ButtonStay consistent with dashboard UI primitives.
- import { Button } from "@workspace/ui/components/button"; + import { Button } from "@/components/ui/button";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx (2)
1-2: Add server-only guard at the top; double-check UI import path consistency
- This page is a Server Component and should start with the runtime guard to prevent accidental client bundling.
- Dashboard guideline prefers importing UI primitives from "@/components/ui/*". If "@workspace/ui" is the new, shared source by design, ignore this; otherwise align the import.
Apply this minimal guard:
+import "server-only"; import { Button } from "@workspace/ui/components/button"; import { PlusIcon } from "lucide-react";If the UI primitives should be local (dashboard), consider:
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
18-28: Nit: add an explicit return type on PageFor clarity and adherence to TS style guidelines, annotate the return type (e.g.,
Promise<JSX.Element>).Example:
export default async function Page(...): Promise<JSX.Element> { // ... }
...am/[team_slug]/[project_slug]/(sidebar)/payments/components/RecentPaymentsSection.client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
Show resolved
Hide resolved
2d4cbd5 to
857727f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx (1)
79-83: Remove server-created ThirdwebClient from client component propsThe
page.tsxunder
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx
is an App Router server component (no"use client"directive). It instantiates a Thirdweb client with your JWT:const client = getClientThirdwebClient({ jwt: authToken, teamId: project.teamId, });Then on lines 142–144 it passes that instance into
<AccountAbstractionAnalytics>:<AccountAbstractionAnalytics client={client} projectId={project.id} teamId={project.teamId} />However,
aa-analytics.tsxstarts with"use client"(a client component) and its props signature doesn’t even includeclient. Passing a server-bound, non-serializable instance (and a secret JWT) into a browser component will:
- Break serialization/hydration in Next.js
- Risk exposing your token to the client
Mandatory fixes:
- In page.tsx, remove
client={client}from<AccountAbstractionAnalytics>(lines 142–144).- Refactor data fetching so that:
- Sensitive calls using the JWT happen server-side (e.g. via a server action or API route using
getAuthToken()andgetClientThirdwebClient()), returning only serializable data.- The client component (
aa-analytics.tsx) instantiates its own ThirdwebClient without a JWT (public config only) and calls your internal endpoints (using afetchJsonwrapper that auto-attaches cookies) to retrieve data.This ensures no secret-bearing client instances cross the Server → Client boundary and keeps your JWT safe.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (1)
1-1: Refactor remaining ThirdwebClient prop usages in transactions componentsI ran the verification script across all
"use client"components in thetransactionssubtree and found these files still declaring aclientprop typed asThirdwebClient, which risks leaking server-only secrets into the client bundle:• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx (line 17)
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx (lines 50, 283)
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-table-ui.tsx (lines 56, 306)
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-table/tx-table.tsx (line 14)
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx (lines 73, 273, 400, 459)
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx (line 38)Please remove these
client: ThirdwebClientprops and instead instantiate or retrieve the vault client within the component (for example, viacreateVaultClient) or through React context/hooks. This ensures no server secrets cross the RSC boundary.
♻️ Duplicate comments (8)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/payments/page.tsx (2)
9-9: Do not pass a JWT-bearing ThirdwebClient into a client componentYou're creating a ThirdwebClient with a server JWT and passing it to a client component (
RouteDiscovery). This risks leaking credentials and can break due to non-serializable props. Refactor to instantiate a browser-safe client inside the client component (e.g., using onlyclientId), or move privileged operations behind server actions/API routes. This was flagged earlier and remains unresolved.Apply in this file:
- import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; + // (removed) Avoid constructing a JWT-bearing client on the server for client-side use. @@ - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: team.id, - }); + // (removed) Create a browser-safe client in the client component instead. @@ - <RouteDiscovery client={client} project={project} /> + <RouteDiscovery project={project} />And update RouteDiscovery (supporting change outside this file):
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/payments/RouteDiscovery.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/payments/RouteDiscovery.tsx @@ -export const RouteDiscovery = ({ - project, - client, -}: { - project: ProjectResponse; - client: ThirdwebClient; -}) => { +'use client'; +import { createThirdwebClient } from 'thirdweb'; +import { useMemo } from 'react'; + +export const RouteDiscovery = ({ project }: { project: ProjectResponse }) => { + const client = useMemo( + () => createThirdwebClient({ clientId: project.publishableKey }), + [project.publishableKey], + );If you prefer not to instantiate the client on the client, route the action through a server action/API that uses the server-side client; pass only serializable identifiers to the client component. I can provide that variant if you want.
Also applies to: 59-63, 82-82
26-28: Fix missing project_slug in login redirect (and exit after redirect)The redirect path omits the project segment, which likely 404s or lands on the wrong scope. Also, explicitly returning after redirect avoids accidental fall-through. This mirrors a previously raised concern in this file.
- loginRedirect(`/team/${team_slug}/settings/payments`); + return loginRedirect(`/team/${team_slug}/${project_slug}/settings/payments`);apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (5)
58-60: Guard against NaN and clamp currentPage ≥ 1Number.parseInt can return NaN; this will break pagination. Parse with radix 10 and default to 1 on invalid input.
Apply:
- const pageSize = 10; - const currentPage = Number.parseInt(searchParams.page ?? "1"); + const pageSize = 10; + const parsed = Number.parseInt(searchParams.page ?? "1", 10); + const currentPage = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
68-75: Fix options shape: use pageSize (camelCase) and remove TS suppressionThe vault SDK expects pageSize, not page_size. The @ts-expect-error is masking the mismatch.
Apply:
- options: { - page: currentPage - 1, - // @ts-expect-error - TODO: fix this - page_size: pageSize, - }, + options: { + page: currentPage - 1, + pageSize, + },
126-135: Do not pass server-created ThirdwebClient into a client component (EngineChecklist)EngineChecklist is a client component (ftux.client.tsx). Passing a server-configured client risks secret leakage and RSC serialization failures. Construct a client inside the client component instead.
Apply in this file:
<EngineChecklist isManagedVault={isManagedVault} - client={client} hasTransactions={hasTransactions} managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} testTxWithWallet={searchParams.testTxWithWallet as string | undefined} wallets={wallets ?? []} />Then, in apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx, instantiate a browser-safe client locally:
"use client"; import { useMemo } from "react"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; // inside EngineChecklist component const client = useMemo(() => getClientThirdwebClient(), []);
145-152: Also remove client prop from TransactionsAnalyticsPageContent and downstream tablesTransactionsAnalyticsPageContent is server, but it forwards client to children that may be client components (e.g., TransactionsTable). Avoid crossing the RSC→client boundary with a jwt-configured client.
Apply here:
- <TransactionsAnalyticsPageContent - client={client} + <TransactionsAnalyticsPageContent project={project} searchParams={searchParams} showAnalytics={hasTransactions && !searchParams.testTxWithWallet} teamSlug={params.team_slug} wallets={wallets} />And in apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx:
-export function TransactionsAnalyticsPageContent(props: { +export function TransactionsAnalyticsPageContent(props: { searchParams: { /* … */ }; project: Project; showAnalytics: boolean; wallets?: Wallet[]; teamSlug: string; - client: ThirdwebClient; }) { return ( <ResponsiveSearchParamsProvider value={props.searchParams}> {/* … */} - <TransactionsTable - client={props.client} + <TransactionsTable project={props.project} teamSlug={props.teamSlug} wallets={props.wallets} />Finally, update any "use client" tables to construct their own client via getClientThirdwebClient().
155-168: Do not pass a server client into ServerWalletsTable (client UI)ServerWalletsTable ultimately renders a “use client” UI (ServerWalletsTableUI). Remove client here and let the UI layer create its own client.
Apply:
- <ServerWalletsTable - client={client} + <ServerWalletsTable currentPage={currentPage} managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} totalPages={Math.ceil(eoas.data.totalRecords / pageSize)} totalRecords={eoas.data.totalRecords} wallets={eoas.data.items as Wallet[]} />And update apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table.tsx:
export function ServerWalletsTable({ wallets, project, teamSlug, currentPage, totalPages, totalRecords, managementAccessToken, - client, }: { wallets: Wallet[]; project: Project; teamSlug: string; managementAccessToken: string | undefined; totalRecords: number; currentPage: number; totalPages: number; - client: ThirdwebClient; }) { return ( <ServerWalletsTableUI - client={client} currentPage={currentPage} managementAccessToken={managementAccessToken} project={project} teamSlug={teamSlug} totalPages={totalPages} totalRecords={totalRecords} wallets={wallets} /> ); }apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx (1)
31-41: Thanks for hardening external links with rel="noopener noreferrer"This addresses prior tab-nabbing concerns for target="_blank".
🧹 Nitpick comments (27)
apps/dashboard/src/@/api/analytics.ts (2)
690-718: Remove leftover webhook latency cache (private, unused).Similar to the summary cache above,
_cached_getWebhookLatencyappears to be dead code after the webhook analytics public APIs were removed. It also keepsWebhookLatencyStatsimported and introduces an error shape inconsistent with the rest of this module.Action:
- Delete this block and remove
WebhookLatencyStatsfrom the import types as noted above.Apply this removal:
-const _cached_getWebhookLatency = unstable_cache( - async ( - params: AnalyticsQueryParams & { webhookId?: string }, - authToken: string, - ): Promise<{ data: WebhookLatencyStats[] } | { error: string }> => { - const searchParams = buildSearchParams(params); - if (params.webhookId) { - searchParams.append("webhookId", params.webhookId); - } - - const res = await fetchAnalytics({ - authToken, - url: `v2/webhook/latency?${searchParams.toString()}`, - init: { - method: "GET", - }, - }); - if (!res.ok) { - const reason = await res.text(); - return { error: reason }; - } - - return (await res.json()) as { data: WebhookLatencyStats[] }; - }, - ["getWebhookLatency"], - { - revalidate: 60 * 60, // 1 hour - }, -);
662-688: Remove orphaned webhook analytics caches (summary & latency).Both
_cached_getWebhookSummaryand_cached_getWebhookLatencyare private, unexported, and no longer referenced outside this file. They incur needless module-eval work, retain dead types in scope, and use an{ error: string }shape that’s inconsistent with theerrorMessagepattern elsewhere.• Delete both cache blocks (lines ~662–688 and ~690–716 in
apps/dashboard/src/@/api/analytics.ts).
• DropWebhookSummaryStatsandWebhookLatencyStatsfrom the top-of-file imports.Suggested diff:
@@ apps/dashboard/src/@/api/analytics.ts -import type { - AnalyticsQueryParams, - EcosystemWalletStats, - EngineCloudStats, - InAppWalletStats, - TransactionStats, - UniversalBridgeStats, - UniversalBridgeWalletStats, - UserOpStats, - WalletStats, - WebhookLatencyStats, - WebhookSummaryStats, -} from "@/types/analytics"; +import type { + AnalyticsQueryParams, + EcosystemWalletStats, + EngineCloudStats, + InAppWalletStats, + TransactionStats, + UniversalBridgeStats, + UniversalBridgeWalletStats, + UserOpStats, + WalletStats, +} from "@/types/analytics"; @@ apps/dashboard/src/@/api/analytics.ts -const _cached_getWebhookSummary = unstable_cache( - async ( - params: AnalyticsQueryParams & { webhookId: string }, - authToken: string, - ): Promise<{ data: WebhookSummaryStats[] } | { error: string }> => { - const searchParams = buildSearchParams(params); - searchParams.append("webhookId", params.webhookId); - - const res = await fetchAnalytics({ - authToken, - url: `v2/webhook/summary?${searchParams.toString()}`, - init: { method: "GET" }, - }); - if (!res.ok) { - const reason = await res.text(); - return { error: reason }; - } - - return (await res.json()) as { data: WebhookSummaryStats[] }; - }, - ["getWebhookSummary"], - { revalidate: 60 * 60 }, -); - -const _cached_getWebhookLatency = unstable_cache( - async ( - params: AnalyticsQueryParams & { webhookId?: string }, - authToken: string, - ): Promise<{ data: WebhookLatencyStats[] } | { error: string }> => { - const searchParams = buildSearchParams(params); - if (params.webhookId) { - searchParams.append("webhookId", params.webhookId); - } - - const res = await fetchAnalytics({ - authToken, - url: `v2/webhook/latency?${searchParams.toString()}`, - init: { method: "GET" }, - }); - if (!res.ok) { - const reason = await res.text(); - return { error: reason }; - } - - return (await res.json()) as { data: WebhookLatencyStats[] }; - }, - ["getWebhookLatency"], - { revalidate: 60 * 60 }, -);apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx (3)
1-1: Add server-only sentinel to guarantee this page stays server-only.This page only redirects; guard it explicitly from accidental client usage.
+import "server-only"; import { redirect } from "next/navigation";
3-5: Annotate the page’s return type as Promise for clarity and TS safety.
redirect()throws; the function never resolves. An explicitPromise<never>matches behavior and our TS guidelines for explicit return types.-export default async function WebhooksPage(props: { - params: Promise<{ team_slug: string; project_slug: string }>; -}) { +export default async function WebhooksPage(props: { + params: Promise<{ team_slug: string; project_slug: string }>; +}): Promise<never> {
8-11: Redirect target confirmed; permanentRedirect optional
- Verified that
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/page.tsxexists and exports a defaultContractsPage.- The current
redirect()call will correctly route to/team/{team_slug}/{project_slug}/webhooks/contracts.Optional refactor: if this redirect is intended to be permanent (i.e. long-lived), consider using Next.js’s
permanentRedirect()in place ofredirect()to return a 301 status and avoid repeated server-side work:-import { redirect } from "next/navigation"; +import { permanentRedirect } from "next/navigation"; - redirect( + permanentRedirect( `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`, );apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/overview/engine-instances-table.tsx (1)
134-136: A11y: mark decorative PlusIcon as hidden from assistive tech.Prevents double-announcement since the button already has a visible text label.
- <Button className="gap-1.5 rounded-full"> - <PlusIcon className="size-4" /> + <Button className="gap-1.5 rounded-full"> + <PlusIcon aria-hidden="true" focusable="false" className="size-4" /> Deploy Dedicated Engine </Button>apps/dashboard/src/@/hooks/useEngine.ts (2)
14-15: Decouple hooks from page components by centralizing engine types under @/types/engineType-only imports still hard-couple this hook to deep app route files. Moving these shared types to a stable barrel avoids brittle relative paths, reduces coupling to UI components, and prevents accidental client/server boundary issues.
Apply this diff in this file:
-import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes"; -import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table"; +import type { ResultItem, EngineStatus } from "@/types/engine";Then add a lightweight types barrel (re-export to avoid duplication now, you can migrate true definitions later):
// apps/dashboard/src/@/types/engine.ts export type { ResultItem } from "@/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes"; export type { EngineStatus } from "@/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table";
76-99: Add sane React Query caching defaults (staleTime ≥ 60s) per guidelinesMost queries here will eagerly refetch on focus because staleTime defaults to 0. For relatively static lists/config, set staleTime ≥ 60_000 or configure defaults at the QueryClient level to reduce network churn and improve UX.
Per-query example:
return useQuery({ enabled: !!instanceUrl, + staleTime: 60_000, queryFn: async () => {Prefer setting global defaults once in your React Query provider:
// e.g. apps/dashboard/src/providers/react-query.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const client = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000, gcTime: 5 * 60_000 }, }, }); export function ReactQueryProvider({ children }: { children: React.ReactNode }) { return <QueryClientProvider client={client}>{children}</QueryClientProvider>; }apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/payments/page.tsx (4)
1-1: Use dashboard UI primitives pathPer app guidelines for the dashboard, import primitives from
@/components/ui/*instead of workspace-level paths.- import { Button } from "@workspace/ui/components/button"; + import { Button } from "@/components/ui/button";
1-3: Mark server component with server-only directiveServer components should start with
import "server-only";to prevent accidental client usage.+import "server-only"; import { Button } from "@/components/ui/button"; import { ArrowLeftIcon } from "lucide-react"; import Link from "next/link";
14-16: Add explicit return type for PageOur TS guideline prefers explicit return types for exported entities. Specify
Promise<JSX.Element>.-export default async function Page(props: { - params: Promise<{ team_slug: string; project_slug: string }>; -}) { +export default async function Page(props: { + params: Promise<{ team_slug: string; project_slug: string }>; +}): Promise<JSX.Element> {
38-58: De-duplicate defaulting logic for feesCurrently you default in both
catchand anif (!fees)block. Collapse into a single nullish-coalescing fallback for clarity.- let fees = await getFees({ - clientId: project.publishableKey, - teamId: team.id, - }).catch(() => { - return { - createdAt: "", - feeBps: 0, - feeRecipient: "", - updatedAt: "", - }; - }); - - if (!fees) { - fees = { - createdAt: "", - feeBps: 0, - feeRecipient: "", - updatedAt: "", - }; - } + const fees = + (await getFees({ + clientId: project.publishableKey, + teamId: team.id, + }).catch(() => null)) ?? { + createdAt: "", + feeBps: 0, + feeRecipient: "", + updatedAt: "", + };apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (2)
22-26: Add explicit return type and extract a props type alias.Guidelines ask for explicit return types and shared/aliased types. This also avoids using the React namespace type directly in the signature.
Apply this diff to the function signature:
-export function ProjectSidebarLayout(props: { - layoutPath: string; - children: React.ReactNode; - hasEngineInstances: boolean; -}) { +export function ProjectSidebarLayout(props: ProjectSidebarLayoutProps): JSX.Element {Add these outside the selected range (top of file):
import type { ReactNode } from "react"; type ProjectSidebarLayoutProps = { layoutPath: string; children: ReactNode; hasEngineInstances: boolean; };
71-78: Gate the “New” badge with a flag so it doesn’t linger.Hard-coding “New” tends to get stale. Consider a local const or remote flag, defaulting off after launch.
Example (add near the top of the component before return):
const SHOW_TOKENS_NEW_BADGE = false;Then update label within the selected range:
- label: ( - <span className="flex items-center gap-2"> - Tokens <Badge>New</Badge> - </span> - ), + label: SHOW_TOKENS_NEW_BADGE ? ( + <span className="flex items-center gap-2"> + Tokens <Badge>New</Badge> + </span> + ) : "Tokens",apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx (3)
56-66: Compute date range once to avoid double work and keep logic DRYMinor clean-up; avoids calling
getLastNDaysRangetwice and clarifies the custom vs preset path.- const range: Range = { - from: - rangeType === "custom" - ? loadedSearchParams.from - : getLastNDaysRange(rangeType).from, - to: - rangeType === "custom" - ? loadedSearchParams.to - : getLastNDaysRange(rangeType).to, - type: rangeType, - }; + const preset = rangeType === "custom" ? null : getLastNDaysRange(rangeType); + const range: Range = { + from: rangeType === "custom" ? loadedSearchParams.from : preset!.from, + to: rangeType === "custom" ? loadedSearchParams.to : preset!.to, + type: rangeType, + };Also, double-check that
searchParamLoaderguaranteesfrom/towhenrangeType === "custom"; otherwise add a fallback.
88-92: Clarify billing gating variable and verify status semanticsRename for readability and confirm whether
pastDueshould also prompt an alert (currently excluded).- const hasSmartWalletsWithoutBilling = + const shouldShowBillingAlert = isBundlerServiceEnabled && team.billingStatus !== "validPayment" && team.billingStatus !== "pastDue"; @@ - {hasSmartWalletsWithoutBilling && ( + {shouldShowBillingAlert && (Follow-up: If
pastDueshould also block mainnet AA, include it in the condition and adjust copy to prompt fixing billing rather than subscribing.Also applies to: 129-129
129-135: Remove spacer div; rely on semantic layout spacingAvoid empty spacer nodes. Wrap the alert and content in one column with gap to maintain consistent rhythm.
- {hasSmartWalletsWithoutBilling && ( - <> - <SmartWalletsBillingAlert teamSlug={params.team_slug} /> - <div className="h-10" /> - </> - )} - <div className="flex grow flex-col gap-10"> + <div className="flex grow flex-col gap-10"> + {hasSmartWalletsWithoutBilling && ( + <SmartWalletsBillingAlert teamSlug={params.team_slug} /> + )} <AccountAbstractionSummary projectId={project.id} teamId={project.teamId} authToken={authToken} /> <AccountAbstractionAnalytics client={client} projectId={project.id} teamId={project.teamId} teamSlug={params.team_slug} userOpStats={userOpStats} /> </div>Also applies to: 135-150
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (1)
164-165: Optional: avoid totalPages = 0 edge caseIf totalRecords is 0, totalPages computes to 0. If the table expects at least one page, clamp to 1.
Apply:
- totalPages={Math.ceil(eoas.data.totalRecords / pageSize)} + totalPages={Math.max(1, Math.ceil(eoas.data.totalRecords / pageSize))}apps/dashboard/src/@/components/blocks/project-page/header/link-group.tsx (4)
114-121: A11y: add aria-labels to dropdown items tooCarry the accessible name into the dropdown items.
Apply:
- <Link + <Link href={link.href} + aria-label={linkTypeToLabel[link.type]} target={isExternal ? "_blank" : undefined} rel={isExternal ? "noopener noreferrer" : undefined} > {linkTypeToIcon[link.type]} {linkTypeToLabel[link.type]} </Link>
81-89: Prefer NavLink for internal routes to get active state stylingFor internal hrefs (starting with "/"), our dashboard guidance is to use NavLink. This improves consistency and active state handling.
Example change:
- <Link + <NavLink href={link.href} aria-label={linkTypeToLabel[link.type]} target={isExternal ? "_blank" : undefined} rel={isExternal ? "noopener noreferrer" : undefined} className="flex flex-row items-center gap-2" > {linkTypeToIcon[link.type]} - </Link> + </NavLink>Remember to add:
import { NavLink } from "@/components/ui/nav-link";Also applies to: 114-121
41-47: Type precision: prefer JSX.Element for icon map valuesUse JSX.Element for the icon map to avoid accidental non-React node assignments.
Apply:
-const linkTypeToIcon: Record<LinkType, React.ReactNode> = { +const linkTypeToIcon: Record<LinkType, JSX.Element> = {
49-55: Annotate return types for exported functionsAdd explicit return types per our TS guidelines.
Apply:
-function orderLinks(links: ActionLink[]) { +function orderLinks(links: ActionLink[]): ActionLink[] { return links.slice().sort((a, b) => { const aIndex = linkTypeToOrder[a.type]; const bIndex = linkTypeToOrder[b.type]; return aIndex - bIndex; }); } -export function LinkGroup(props: { links: ActionLink[] }) { +export function LinkGroup(props: { links: ActionLink[] }): JSX.Element {Also applies to: 62-129
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (1)
36-38: Ensure loginRedirect short-circuits executionloginRedirect likely throws a redirect, but we’ve standardized on returning after calling it in server components to avoid accidental fallthrough if behavior changes.
Apply:
if (!authToken) { - loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`); + loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`); + return null as any; }apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx (4)
1-1: Align Button import with dashboard UI guidelinesDashboard files should import primitives from "@/components/ui/*". Please switch the Button import to the local UI entry to keep bundle consistency and theming aligned.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
26-44: Prefer NavLink for internal navigation (active-state + consistency)Per dashboard guidelines and past learnings, use NavLink for internal routes so active states and class merging are handled uniformly. Keep next/link for external targets.
import Link from "next/link"; +import { NavLink } from "@/components/ui/NavLink"; function Action(props: { action: Action; variant?: "default" | "secondary" }) { const action = props.action; return "component" in action ? ( action.component ) : ( <Button asChild className="rounded-full" variant={props.variant}> - <Link - href={action.href} - target={action.external ? "_blank" : undefined} - rel={action.external ? "noopener noreferrer" : undefined} - className="flex flex-row items-center gap-1.5" - > - {action.icon} - {action.label} - {action.external && <ArrowUpRightIcon className="size-4" />} - </Link> + {action.external ? ( + <Link + href={action.href} + target="_blank" + rel="noopener noreferrer" + className="flex flex-row items-center gap-1.5" + > + {action.icon} + {action.label} + <ArrowUpRightIcon className="size-4" /> + </Link> + ) : ( + <NavLink href={action.href} className="flex flex-row items-center gap-1.5"> + {action.icon} + {action.label} + </NavLink> + )} </Button> ); }
8-25: Simplify/relax Action type and add a11y hook (ariaLabel)Forbidding icons on external actions is unnecessarily restrictive and complicates the union. Consider a base shape shared by both internal/external and allow an optional ariaLabel for assistive tech. Keeps the runtime logic simpler.
-type Action = - | ({ - label: string; - href: string; - } & ( - | { - external: true; - icon?: never; - } - | { - icon?: React.ReactNode; - external?: false; - } - )) - | { - component: React.ReactNode; - }; +type ActionBase = { + label: string; + href: string; + icon?: React.ReactNode; + external?: boolean; // default false + ariaLabel?: string; +}; +type Action = + | ActionBase + | { + component: React.ReactElement; + };And wire ariaLabel where rendered:
- <Link + <Link href={action.href} target={action.external ? "_blank" : undefined} rel={action.external ? "noopener noreferrer" : undefined} + aria-label={action.ariaLabel} className="flex flex-row items-center gap-1.5" >
91-104: Defensive rendering is fine; consider simplifying with required primarySince ProjectPageHeaderProps.actions.primary is required when actions is non-null, the extra truthy checks are redundant. Safe to keep; minor simplification possible.
- {props.actions.primary && <Action action={props.actions.primary} />} + <Action action={props.actions.primary} />
apps/dashboard/src/@/components/blocks/project-page/header/link-group.tsx
Show resolved
Hide resolved
apps/dashboard/src/@/components/blocks/project-page/header/link-group.tsx
Show resolved
Hide resolved
apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx
Show resolved
Hide resolved
...shboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx
Show resolved
Hide resolved
.../src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx
Show resolved
Hide resolved
857727f to
a5e97e2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx (1)
91-97: Avoid passing server JWTs to client componentsThe
authTokenprop is a sensitive server‐side JWT. Exposing it in client components leaks the token to the browser and DevTools. We need to remove it from allCustomChatButtonusages and instead have that component fetch its own ephemeral token via an internal API route or server action.Files requiring changes:
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx (line 92)
- apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx (line 108)
Suggested minimal diff for each occurrence:
<CustomChatButton - authToken={authToken} clientId={project.publishableKey} examplePrompts={siwaExamplePrompts} label="Ask AI Assistant" team={team} />Refactor steps:
- In these layouts, stop passing
authTokenintoCustomChatButton.- Inside
CustomChatButton(client component), call a server action or hit an internal API route (e.g./api/chat/token) that usesgetAuthToken()on the server to return a short‐lived token.- Only pass non‐sensitive props (like
clientId,examplePrompts,team) from the server.Additionally, please verify that no other client components are receiving
authTokendirectly and refactor them similarly.apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx (1)
37-51: Wire ImportModal to the locally constructed clientAfter moving client construction inside this component, pass that local client to the modal.
- <ImportModal - client={props.client} + <ImportModal + client={client} isOpen={importModalOpen} onClose={() => { setImportModalOpen(false); }}apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (1)
36-38: Add a return after loginRedirect to prevent fallthroughPer prior incidents, ensure no code runs after loginRedirect in server components.
if (!authToken) { - loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`); + return loginRedirect( + `/team/${params.team_slug}/${params.project_slug}/tokens`, + ); }apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/PaymentLinksTable.client.tsx (1)
317-318: Fix button label: “Delete Webhook” → “Delete Payment”Dialog context is deleting a payment link. The current label is misleading.
- Delete Webhook + Delete Payment
♻️ Duplicate comments (18)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (5)
58-60: Harden page parsing: handle arrays, invalid values, radix, and clamp to >= 1
Number.parseInt(searchParams.page ?? "1")can yieldNaN(e.g.,?page=foo). Also,searchParams.pagecan be an array. Parse with explicit radix, default, and clamp.- const pageSize = 10; - const currentPage = Number.parseInt(searchParams.page ?? "1"); + const pageSize = 10; + const pageParam = Array.isArray(searchParams.page) + ? searchParams.page[0] + : searchParams.page; + const parsed = Number.parseInt(pageParam ?? "1", 10); + const currentPage = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
68-75: Align Vault SDK options (camelCase) and remove@ts-expect-errorThe SDK expects
pageSize(camelCase). Also clamppageto >= 0 to avoid negative pages whencurrentPage === 1.- options: { - page: currentPage - 1, - // @ts-expect-error - TODO: fix this - page_size: pageSize, - }, + options: { + page: Math.max(0, currentPage - 1), + pageSize, + },
126-135: Critical: Do not pass a server-createdThirdwebClientormanagementAccessTokento a client component (EngineChecklist)
EngineChecklistis a client component (ftux.client.tsx). Passingclient(configured with JWT) ormanagementAccessTokencrosses the RSC→client boundary and can leak secrets and fail serialization.Apply minimal safe changes here:
<EngineChecklist isManagedVault={isManagedVault} - client={client} hasTransactions={hasTransactions} - managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} testTxWithWallet={searchParams.testTxWithWallet as string | undefined} wallets={wallets ?? []} />Then in
ftux.client.tsx, construct the client on the client and proxy privileged actions via server actions/API routes (so the token never hits the browser):// apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx "use client"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; // import { createServerWallet } from "@/app/api/server-wallets/actions"; // server action you define export const EngineChecklist: React.FC<PropsWithoutSecrets> = (props) => { const client = getClientThirdwebClient(); // no JWT here // For actions requiring managementAccessToken, call server action: // await createServerWallet({ projectId: props.project.id, ... }) … };Verification helper (find any client components still accepting a
clientormanagementAccessTokenprop):#!/bin/bash # Find "use client" files in transactions subtree and grep for props named client or managementAccessToken rg -n '^"use client"' -g 'apps/dashboard/src/app/**/transactions/**/*.tsx' | cut -d: -f1 | sort -u \ | xargs -I{} rg -n --no-heading -C1 '\b(client|managementAccessToken)\s*[:=]' {} || true
145-152: Do not forward serverclientintoTransactionsAnalyticsPageContent(it fans out to client tables)
TransactionsAnalyticsPageContentis server, but it forwardsclienttoTransactionsTable, which is likely client. Removeclientfrom this prop chain and instantiate a client within client components without secrets.<TransactionsAnalyticsPageContent - client={client} project={project} searchParams={searchParams} showAnalytics={hasTransactions && !searchParams.testTxWithWallet} teamSlug={params.team_slug} wallets={wallets} />Follow-up change (drop
clientfrom the server component’s props and stop forwarding to children):// apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx export function TransactionsAnalyticsPageContent(props: { searchParams: { from?: string | string[]; to?: string | string[]; interval?: string | string[] }; project: Project; showAnalytics: boolean; wallets?: Wallet[]; teamSlug: string; - client: ThirdwebClient; }) { … - <TransactionsTable client={props.client} … /> + <TransactionsTable … /> }Verification helper (find where
clientis still threaded through analytics):#!/bin/bash rg -n --no-heading -C2 '\bTransactionsAnalyticsPageContent\b|\bTransactionsTable\b' apps/dashboard/src/app -S rg -n --no-heading -C1 '\bclient\s*=' apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions -S || true
155-168: Critical: Don’t passmanagementAccessTokenor serverclienttoServerWalletsTable(and its UI is client-side)
ServerWalletsTableUIis a client component; passingmanagementAccessTokenor a JWT-configuredclientleaks secrets. Keep privileged operations on the server via actions/API and pass only safe primitives.Apply in this file:
<ServerWalletsTable - client={client} currentPage={currentPage} - managementAccessToken={managementAccessToken ?? undefined} project={project} teamSlug={params.team_slug} totalPages={Math.ceil(eoas.data.totalRecords / pageSize)} totalRecords={eoas.data.totalRecords} wallets={eoas.data.items as Wallet[]} />Update the table wrapper to drop secret-bearing props and stop forwarding to the client UI:
-// apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table.tsx export function ServerWalletsTable({ wallets, project, teamSlug, currentPage, totalPages, totalRecords, - managementAccessToken, - client, }: { wallets: Wallet[]; project: Project; teamSlug: string; - managementAccessToken: string | undefined; totalRecords: number; currentPage: number; totalPages: number; - client: ThirdwebClient; }) { return ( <ServerWalletsTableUI - client={client} currentPage={currentPage} - managementAccessToken={managementAccessToken} project={project} teamSlug={teamSlug} totalPages={totalPages} totalRecords={totalRecords} wallets={wallets} /> ); }Then, in
ServerWalletsTableUI(client), instantiate a client locally and call server actions for any operation that needsmanagementAccessToken. Do not accept or read secrets from props.Verification helper:
#!/bin/bash # Ensure no client components accept `managementAccessToken` or `client` props. rg -n '^"use client"' -g 'apps/dashboard/src/app/**/transactions/**/*.tsx' | cut -d: -f1 | sort -u \ | xargs -I{} rg -n --no-heading -C1 '\b(managementAccessToken|client)\s*:' {} || trueapps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (1)
48-51: Restore explicit isActive for Transactions to keep parent highlighted on nested routes.Without a custom matcher, nested routes like /…/transactions/123 may not mark “Transactions” active.
{ - href: `${props.layoutPath}/transactions`, + href: `${props.layoutPath}/transactions`, icon: ArrowLeftRightIcon, - label: "Transactions", + label: "Transactions", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/transactions`), },apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx (1)
84-87: Guard against undefined services and use .some for booleansUse optional chaining and .some to convey intent and avoid truthiness casting.
-const isBundlerServiceEnabled = !!project.services.find( - (s) => s.name === "bundler", -); +const isBundlerServiceEnabled = !!project.services?.some( + (s) => s.name === "bundler", +);apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx (1)
3-3: Use dashboard UI primitives to avoid pulling shared workspace bundleIn apps/dashboard, prefer "@/components/ui/" over "@workspace/ui/" for consistency and bundle hygiene.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (3)
1-2: Use dashboard UI primitives instead of workspace UIImport Button from "@/components/ui/button" to avoid pulling the shared workspace UI into the dashboard bundle.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
53-111: Remove server-created client from header/actions; construct clients inside client componentsHeader actions render client components (Dialog contents, Import button). Passing a JWT-backed client here risks leakage. Remove the client prop from the header, Cards, and ImportTokenButton calls.
<ProjectPage header={{ - client, title: "Tokens", description: "Create and manage tokens for your project", actions: { primary: { component: ( <Dialog> <DialogTrigger asChild> <Button className="gap-1.5 rounded-full"> <PlusIcon className="size-4" /> Create Token </Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Create Token</DialogTitle> </DialogHeader> <Cards - client={client} projectId={project.id} projectSlug={params.project_slug} teamId={team.id} teamSlug={params.team_slug} /> </DialogContent> </Dialog> ), }, secondary: { component: ( <ImportTokenButton - client={client} projectId={project.id} projectSlug={params.project_slug} teamId={team.id} teamSlug={params.team_slug} /> ), }, },
112-121: Stop threading client through AssetsPageAsync and ContractTableKeep JWT on the server. Don’t pass clients down; let client-side tables construct their own safe client.
<Suspense fallback={<GenericLoadingPage />}> <AssetsPageAsync authToken={authToken} - client={client} projectId={project.id} projectSlug={params.project_slug} teamId={team.id} teamSlug={params.team_slug} /> </Suspense>-async function AssetsPageAsync(props: { +async function AssetsPageAsync(props: { teamId: string; projectId: string; authToken: string; - client: ThirdwebClient; teamSlug: string; projectSlug: string; }) {<ClientOnly ssr={<GenericLoadingPage />}> <ContractTable - client={props.client} contracts={deployedContracts} pageSize={10} projectId={props.projectId} projectSlug={props.projectSlug} teamId={props.teamId} teamSlug={props.teamSlug} variant="asset" /> </ClientOnly>Also applies to: 143-145, 126-133
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/payments/page.tsx (1)
33-64: Do not pass a secret-backed client into header; it may reach client components indirectlyYou construct client with a JWT (secretKey) and pass it via header={{ client, ... }}. Previous review found ProjectPageHeader forwards client to a client component (ProjectAvatar), risking secret exposure. Keep the secret client server-only.
Minimal fix here: stop passing client into the header.
return ( <ProjectPage header={{ - client, title: "Webhooks", description: "Get notified for Bridge, Swap and Onramp events.", actions: {If the avatar needs chain data, hydrate a browser-safe client within that client component (no jwt) or fetch server-side and pass plain data.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx (1)
56-92: Don’t pass secret client into ProjectPage headerAs noted elsewhere, header can forward client into client components. Remove the client prop from header to avoid secret leakage.
- <ProjectPage - header={{ - client, + <ProjectPage + header={{ title: "Wallets", description: "Create wallets for your users with flexible authentication options. Choose from email/phone verification, OAuth, passkeys, or external wallet connections.",apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/RecentPaymentsSection.client.tsx (2)
19-25: Remove JWT from client component propsThis prop causes the JWT to be serialized to the browser. Never pass secrets to client components.
export function RecentPaymentsSection(props: { projectClientId: string; teamSlug: string; projectSlug: string; teamId: string; - jwt: string; }) {
46-49: Do not construct a secret client in the browsergetClientThirdwebClient({ jwt }) seeds a secretKey on the client. Create a browser-safe client with no jwt, or require any privileged operations via server actions/internal APIs.
- const client = getClientThirdwebClient({ - jwt: props.jwt, - teamId: props.teamId, - }); + // Browser-safe: no jwt/secretKey + const client = getClientThirdwebClient();apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx (3)
56-104: Don’t pass a secret-backed client into ProjectPage headerPassing client here risks it being forwarded to client components (as previously found in ProjectPageHeader → ProjectAvatar). Keep the secret client server-only.
<ProjectPage header={{ - client, title: "Payments", description: "Payments allow you to create advanced payment flows to monetize your app through product sales, peer to peer payments, token sales, and more.",
170-176: Critical: JWT passed to a client component (RecentPaymentsSection)RecentPaymentsSection is a client component; passing jwt={authToken} leaks credentials. Remove the prop and switch the component to a browser-safe client internally (no jwt).
<RecentPaymentsSection - jwt={authToken} projectClientId={project.publishableKey} teamId={project.teamId} projectSlug={params.project_slug} teamSlug={params.team_slug} />
161-169: Do not pass server-sideclientorauthTokeninto client componentsWe’ve confirmed that in
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PayAnalytics.tsx
the server-onlyPayAnalyticsis forwarding sensitive props to client components:•
PayCustomersTable(has"use client"at the top of its file)
– Usage: lines 131–132
– Props passed:client={props.client}
•PaymentHistory(implemented inPaymentHistory.client.tsx)
– Usage: lines 133–136
– Props passed:client={props.client},authToken={props.authToken}This leaks your JWT-bearing Thirdweb client and auth token into browser code. To fix:
- Move all data fetching (using
clientandauthToken) into a server action or internal API route.- Return only plain JSON data to the client.
- Update
PaymentHistoryandPayCustomersTableto accept sanitized data props (e.g.historyData,customerData) instead ofclient/authToken.- If you need a Thirdweb client in the browser for formatting/UI tasks, instantiate it without a JWT.
Example diff:
- <PayCustomersTable client={props.client} data={walletData || []} /> + <PayCustomersTable data={walletData || []} /> - <PaymentHistory - client={props.client} - projectClientId={props.projectClientId} - teamId={props.teamId} - authToken={props.authToken} - … - /> + <PaymentHistory historyData={paymentHistoryData} />
🧹 Nitpick comments (38)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (1)
1-1: Addimport "server-only";to assert server component boundaryThis page runs on the server. Add the directive to prevent accidental client-only imports and improve static analysis.
+import "server-only"; import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx (4)
22-26: Add explicit return type on the component.Align with repo TS guidelines by annotating the component’s return type.
-export function ProjectSidebarLayout(props: { +export function ProjectSidebarLayout(props: { layoutPath: string; children: React.ReactNode; hasEngineInstances: boolean; -}) { +}): JSX.Element {
42-57: Unify active-state behavior across sidebar links (wallets/contracts too).To avoid inconsistent highlighting for child pages across sections, apply startsWith matchers for items with nested routes (Wallets, Contracts, etc.). Keep using exactMatch: true for Overview.
Diff for this block (repeat the pattern for other sections below):
{ - href: `${props.layoutPath}/wallets`, + href: `${props.layoutPath}/wallets`, icon: WalletProductIcon, - label: "Wallets", + label: "Wallets", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/wallets`), }, @@ { - href: `${props.layoutPath}/contracts`, + href: `${props.layoutPath}/contracts`, icon: ContractIcon, - label: "Contracts", + label: "Contracts", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/contracts`), },Optional helper to reduce repetition (place near the top of the file):
const startsWithActive = (base: string) => (pathname: string) => pathname.startsWith(base);Then use:
isActive: startsWithActive(${props.layoutPath}/wallets), etc.
65-79: Apply the same active-state pattern for Monetize links.Payments and Tokens likely have nested routes; mirror the Transactions pattern for consistent UX.
{ - href: `${props.layoutPath}/payments`, + href: `${props.layoutPath}/payments`, icon: PayIcon, - label: "Payments", + label: "Payments", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/payments`), }, { - href: `${props.layoutPath}/tokens`, + href: `${props.layoutPath}/tokens`, icon: TokenIcon, label: ( <span className="flex items-center gap-2"> Tokens <Badge>New</Badge> </span> ), + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/tokens`), },
85-118: Apply startsWith active-state to Scale links, including conditional Engine.Prevents parent item from losing highlight on nested pages.
{ - href: `${props.layoutPath}/insight`, + href: `${props.layoutPath}/insight`, icon: InsightIcon, - label: "Insight", + label: "Insight", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/insight`), }, { - href: `${props.layoutPath}/account-abstraction`, + href: `${props.layoutPath}/account-abstraction`, icon: SmartAccountIcon, - label: "Account Abstraction", + label: "Account Abstraction", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/account-abstraction`), }, { - href: `${props.layoutPath}/rpc`, + href: `${props.layoutPath}/rpc`, icon: RssIcon, - label: "RPC", + label: "RPC", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/rpc`), }, { - href: `${props.layoutPath}/vault`, + href: `${props.layoutPath}/vault`, icon: LockIcon, - label: "Vault", + label: "Vault", + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/vault`), }, ...(props.hasEngineInstances ? [ { - href: `${props.layoutPath}/engine`, + href: `${props.layoutPath}/engine`, icon: DatabaseIcon, label: ( <span className="flex items-center gap-2">Engine</span> ), + isActive: (pathname) => + pathname.startsWith(`${props.layoutPath}/engine`), }, ] : []),apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx (1)
1-1: Declare this as a Server Component explicitly.Add the server-only directive to prevent accidental client-side imports/bundling.
+import "server-only"; import { redirect } from "next/navigation";apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx (4)
125-152: Add explicit return type to useActiveShadcnSidebarLink for clarity and safetyExplicit return types are part of our TSX guidelines and help prevent accidental widening to
any.Apply:
-function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) { +function useActiveShadcnSidebarLink( + links: ShadcnSidebarLink[], +): ShadcnSidebarBaseLink | undefined { const pathname = usePathname(); - const activeLink = useMemo(() => { + const activeLink = useMemo<ShadcnSidebarBaseLink | undefined>(() => { function isActive(link: ShadcnSidebarBaseLink) { if (link.exactMatch) { return link.href === pathname; } return pathname?.startsWith(link.href); } @@ - }, [links, pathname]); + }, [links, pathname]); return activeLink; }
154-169: Add explicit return type to useIsSubnavActiveKeeps the hook contract obvious and avoids unintended type widening.
-function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) { +function useIsSubnavActive(links: ShadcnSidebarBaseLink[]): boolean { const pathname = usePathname(); - const isSubnavActive = useMemo(() => { + const isSubnavActive = useMemo<boolean>(() => { function isActive(link: ShadcnSidebarBaseLink) { if (link.exactMatch) { return link.href === pathname; } return pathname?.startsWith(link.href); } return links.some(isActive); }, [links, pathname]); return isSubnavActive; }
283-289: Key template has an extra literal dollar ($) — simplify
key={submenu_$$${idx}}renders a literal$before the index (valid but odd). For consistency with other keys, drop the extra$.- key={`submenu_$${ + key={`submenu_${ // biome-ignore lint/suspicious/noArrayIndexKey: index is fine here idx }`}Or more explicit:
- key={`submenu_$${ - idx - }`} + key={`submenu_${idx}`}
94-94: Confirm AppFooter’s Default Container BehaviorAppFooter’s implementation always wraps its inner content in a
<div>with thecontainerclass (and associated max-width breakpoints), regardless of whether you passcontainerClassName. Removing the (non-existent)containerClassName="max-w-7xl"override in FullWidthSidebarLayout therefore does not change how the footer’s content is constrained. No other call sites overridecontainerClassName, so there’s no risk of regressions.• File requiring review:
- apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx (around line 94)
• AppFooterProps (in apps/dashboard/src/@/components/footers/app-footer.tsx):
–containerClassName?: stringis optional, and by defaultAppFooterprepends"container flex …"to its inner wrapper.
• No usages ofcontainerClassNamefound across dashboard call sites.Optional refinement:
If you truly want edge-to-edge inner content (rather than the default capped container), either override the prop or wrap accordingly. For example:- <AppFooter /> + {/* full-width background; remove container cap on inner content */} + <AppFooter containerClassName="!max-w-full p-0" />apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/webhooks/components/webhooks.client.tsx (8)
159-196: Align query invalidation key with the fetch key (include teamId).Your list query key is ["webhooks", clientId, teamId], but the create invalidation uses ["webhooks", clientId]. Partial invalidation will work, but being explicit prevents accidental over‑invalidations if new subkeys are added later.
Apply:
return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], + queryKey: ["webhooks", props.clientId, props.teamId], });
338-343: Avoid double invalidation on delete; centralize toast/close in mutation onSuccess and include teamId.Currently you invalidate in both the mutation’s onSuccess and the mutateAsync onSuccess, causing two refetches. Consolidate and match the fetch key.
const deleteMutation = useMutation({ mutationFn: async (id: string) => { await deleteWebhook({ clientId: props.clientId, teamId: props.teamId, webhookId: id, }); return null; }, - onSuccess: () => { - return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], - }); - }, + onSuccess: () => { + toast.success("Webhook deleted successfully"); + setOpen(false); + return queryClient.invalidateQueries({ + queryKey: ["webhooks", props.clientId, props.teamId], + }); + }, }); @@ - onClick={() => { - deleteMutation.mutateAsync(props.webhookId, { - onError(err) { - toast.error("Failed to delete webhook", { - description: err instanceof Error ? err.message : undefined, - }); - }, - onSuccess: () => { - toast.success("Webhook deleted successfully"); - setOpen(false); - return queryClient.invalidateQueries({ - queryKey: ["webhooks", props.clientId], - }); - }, - }); - }} + onClick={() => { + deleteMutation.mutateAsync(props.webhookId, { + onError(err) { + toast.error("Failed to delete webhook", { + description: err instanceof Error ? err.message : undefined, + }); + }, + }); + }}Also applies to: 361-373
264-273: Don’t spread RHF field props onto Select; pass only value/onValueChange.Spreading {...field} can leak name/ref/onBlur into a custom Select that doesn’t forward them, causing warnings or broken controlled state. Use value/onValueChange explicitly (shadcn pattern).
- <Select {...field} onValueChange={field.onChange}> + <Select value={field.value} onValueChange={field.onChange}> <SelectTrigger className="w-full"> <SelectValue placeholder="v2" /> </SelectTrigger>
112-116: Ensure createdAt is a Date; wrap with new Date() to be safe.If createdAt is an ISO string, formatDistanceToNow will throw. Wrapping is safe for both Date and string.
- {formatDistanceToNow(webhook.createdAt, { addSuffix: true })} + {formatDistanceToNow(new Date(webhook.createdAt), { addSuffix: true })}
152-157: Require HTTPS for webhook URL.Security nit: Webhook endpoints should be HTTPS. Tighten zod validation to enforce scheme.
- url: z.string().url("Please enter a valid URL."), + url: z + .string() + .url("Please enter a valid URL.") + .refine((v) => v.startsWith("https://"), "URL must use https."),
210-221: Simplify form reset logic after success.Multiple setValue calls after reset are redundant. Reset with explicit defaults once and then flip secretStored.
- onSuccess: () => { - setOpen(false); - setSecretStored(false); - toast.success("Webhook created successfully"); - form.reset(); - form.clearErrors(); - form.setValue("url", ""); - form.setValue("label", ""); - form.setValue("version", "2"); - form.setValue("secret", undefined); - }, + onSuccess: () => { + setOpen(false); + toast.success("Webhook created successfully"); + form.reset({ label: "", url: "", version: "2", secret: undefined }); + setSecretStored(false); + },
65-73: Add staleTime and an error state for the webhooks query.
- staleTime ≥ 60s aligns with dashboard caching guidance.
- Provide a simple retry UI on error.
const webhooksQuery = useQuery({ @@ - queryKey: ["webhooks", props.clientId, props.teamId], + queryKey: ["webhooks", props.clientId, props.teamId], + staleTime: 60_000, }); @@ if (webhooksQuery.isPending) { return <GenericLoadingPage />; } + if (webhooksQuery.isError) { + return ( + <div className="flex flex-col items-center gap-4 rounded-lg border border-border p-8 text-center"> + <h2 className="font-semibold text-xl">Failed to load webhooks.</h2> + <Button onClick={() => webhooksQuery.refetch()}>Retry</Button> + </div> + ); + }Also applies to: 75-77, 79-94
103-103: Nit: Prefer “URL” over “Url” in table header.- <TableHead>Url</TableHead> + <TableHead>URL</TableHead>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx (7)
1-1: Mark as server component explicitlyAdd the server-only import to catch accidental client-only imports at build time.
+import "server-only"; import type { Metadata } from "next";
20-23: Prefer a type alias over interface for simple shapesMatches the repo guideline to use type aliases unless you need nominal typing.
-interface PageParams { - team_slug: string; - project_slug: string; -} +type PageParams = { + team_slug: string; + project_slug: string; +};
25-29: Annotate the return type of PageBe explicit per guidelines; this also helps catch accidental non-JSX returns.
-export default async function Page(props: { +export default async function Page(props: { params: Promise<PageParams>; searchParams: Promise<SearchParams>; children: React.ReactNode; -}) { +}): Promise<JSX.Element> {
56-66: Avoid duplicate calls to getLastNDaysRangeCompute it once for readability and micro-optim.
-const range: Range = { - from: - rangeType === "custom" - ? searchParams.from - : getLastNDaysRange(rangeType).from, - to: - rangeType === "custom" - ? searchParams.to - : getLastNDaysRange(rangeType).to, - type: rangeType, -}; +const baseRange = + rangeType === "custom" + ? { from: searchParams.from, to: searchParams.to } + : getLastNDaysRange(rangeType); + +const range: Range = { + ...baseRange, + type: rangeType, +};
88-92: Confirm billing gating semantics for pastDueCurrent logic suppresses the alert for pastDue. If the intent is “show alert for anything that is not validPayment,” include pastDue in the failing states. If pastDue has its own global banner, leaving it out here is fine. Please confirm.
If you want a single source of truth for “billing OK” statuses:
-const hasSmartWalletsWithoutBilling = - isBundlerServiceEnabled && - team.billingStatus !== "validPayment" && - team.billingStatus !== "pastDue"; +const BILLING_OK = new Set<Exclude<typeof team.billingStatus, undefined>>([ + "validPayment", + // add other “OK” statuses here if any +]); +const hasSmartWalletsWithoutBilling = + isBundlerServiceEnabled && !BILLING_OK.has(team.billingStatus);
129-134: Avoid decorative spacer divsPrefer semantic separators or margin on adjacent blocks to keep markup meaningful.
-<> - <SmartWalletsBillingAlert teamSlug={params.team_slug} /> - <div className="h-10" /> -</> +<> + <SmartWalletsBillingAlert teamSlug={params.team_slug} /> + {/* vertical rhythm */} + <div className="mt-6" /> +</>If you want a true rule, import and use the design-system Separator:
import { Separator } from "@/components/ui/separator";- <div className="h-10" /> + <Separator className="my-6" />
154-174: Use metadataBase for OG images and streamline URL handlingYou can simplify your metadata by setting a single metadataBase and then using relative paths for your Open Graph images. This reduces duplication and ensures consistency across your pages. The updated snippet would look like:
export const metadata: Metadata = { + metadataBase: new URL(getAbsoluteUrl()), // ← base URL for all relative metadata URLs description: seo.desc, title: seo.title, openGraph: { title: seo.title, description: seo.desc, - images: [ - { - url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`, - width: 1200, - height: 630, - alt: seo.title, - }, - ], + images: [ + { + url: "/assets/og-image/dashboard-wallets-smart-wallet.png", + width: 1200, + height: 630, + alt: seo.title, + }, + ], }, };I confirmed that getAbsoluteUrl is defined in
apps/dashboard/src/@/utils/vercel.tsand does not import or callnext/headers; it relies only onwindow.location.origin(in the browser) or environment variables on the server, so it’s safe for static metadata.This refactor is optional but recommended for cleaner, more maintainable metadata configuration.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx (1)
15-15: Add explicit return types to exported components (minor)Keeps signatures self-documenting and aligns with the repo’s TS guidance.
-export function ImportTokenButton(props: { +export function ImportTokenButton(props: { }) { + // : React.JSX.Element-export function Cards(props: { +export function Cards(props: { }) { + // : React.JSX.ElementAlso applies to: 56-56
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/page.tsx (4)
1-1: Add server-only guard at top of server component.Per the dashboard guidelines, server components should start with
import "server-only";to prevent accidental client imports.Apply this diff:
+import "server-only"; import { notFound } from "next/navigation";
10-12: Add an explicit return type to the page function.Explicit return types are required by the TS/TSX guidelines; declare
Promise<JSX.Element>for clarity and tooling.-export default async function ContractsPage(props: { - params: Promise<{ team_slug: string; project_slug: string }>; -}) { +export default async function ContractsPage(props: { + params: Promise<{ team_slug: string; project_slug: string }>; +}): Promise<JSX.Element> {
15-16: Parallelize project and supported-chain fetches to reduce TTFB.
getSupportedWebhookChains()doesn’t depend onproject; fetch both concurrently after resolvingparamsandauthToken.- const project = await getProject(params.team_slug, params.project_slug); + const [project, supportedChainsRes] = await Promise.all([ + getProject(params.team_slug, params.project_slug), + getSupportedWebhookChains(), + ]); @@ - let supportedChainIds: number[] = []; - const supportedChainsRes = await getSupportedWebhookChains(); - if ("chains" in supportedChainsRes) { - supportedChainIds = supportedChainsRes.chains; - } + let supportedChainIds: number[] = []; + if ("chains" in supportedChainsRes) { + supportedChainIds = supportedChainsRes.chains; + }Also applies to: 26-31
49-60: Avoid duplicate documentation entry (secondary action + links.docs).Both the secondary action and
linkspoint to the same docs URL. Keep one to reduce UI noise; I recommend keeping the secondary action button and dropping the redundantlinksitem.}, - links: [ - { - type: "docs", - href: "https://portal.thirdweb.com/insight/webhooks", - }, - ],apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/PaymentLinksTable.client.tsx (5)
44-55: Avoid duplicating page-level headings inside the table componentProject pages now provide a header via ProjectPage. Rendering another H2 + subtitle here causes redundant headings and extra vertical chrome. Prefer letting the page header own the title/description and keep this component focused on the table.
Apply this minimal change:
export function PaymentLinksTable(props: { clientId: string; teamId: string }) { - return ( - <section> - <div className="mb-4"> - <h2 className="font-semibold text-xl tracking-tight">Payment Links</h2> - <p className="text-muted-foreground text-sm"> - Payment links you have created in this project. - </p> - </div> - <PaymentLinksTableInner clientId={props.clientId} teamId={props.teamId} /> - </section> - ); + return <PaymentLinksTableInner clientId={props.clientId} teamId={props.teamId} />; }
68-91: Gate the usages query on the links query to avoid unnecessary extra fetchesWhen there are zero links or while links are still loading, the usages query still runs (albeit with an empty array). Add an enabled flag and derive a stable key to avoid churn.
const paymentLinkUsagesQuery = useQuery({ queryFn: async () => { const paymentLinks = paymentLinksQuery.data || []; return await Promise.all( paymentLinks.map(async (paymentLink) => { const { data } = await getPayments({ clientId: props.clientId, teamId: props.teamId, paymentLinkId: paymentLink.id, }); return { paymentLink, usages: data, }; }), ); }, - queryKey: [ + queryKey: [ "payment-link-usages", paymentLinksQuery.dataUpdatedAt, props.clientId, props.teamId, ], + enabled: !!paymentLinksQuery.data && !paymentLinksQuery.isLoading, });
118-121: Tighten empty-state copy to match the featureThis section manages “Payment Links,” not generic “payments.”
- title="No payments configured yet" - description="Create a payment to receive any token in seconds." + title="No payment links yet" + description="Create a payment link to receive any token in seconds."
184-203: Revenue calculation: formatting and token/decimals consistency
- You sum Number(toTokens(...)) and then call toString(), which can lose precision and produce inconsistent formatting vs. formatTokenAmount used elsewhere.
- Assumes usages’ destinationToken matches the link’s destinationToken; if that invariant can break, the sum will mix different tokens/decimals.
Recommend:
- Use the link’s decimals consistently and format via formatTokenAmount for parity with the “Amount” column.
- If mixing tokens is possible, either filter to matching token or display “—”.
Proposed formatting tweak (still numeric, but consistent):
- `${( - paymentLinkUsagesQuery.data - ?.find((x) => x.paymentLink.id === paymentLink.id) - ?.usages?.reduce( - (acc, curr) => - acc + - Number( - toTokens( - curr.destinationAmount, - curr.destinationToken.decimals, - ), - ), - 0, - ) || 0 - ).toString()} ${paymentLink.destinationToken.symbol}` + {formatTokenAmount( + String( + (paymentLinkUsagesQuery.data + ?.find((x) => x.paymentLink.id === paymentLink.id) + ?.usages?.reduce( + (acc, curr) => + acc + + Number( + toTokens( + curr.destinationAmount, + paymentLink.destinationToken.decimals, + ), + ), + 0, + ) ?? 0), + ), + )}{" "} + {paymentLink.destinationToken.symbol}If different tokens per usage are possible, guard:
const usagesForLink = paymentLinkUsagesQuery.data?.find((x) => x.paymentLink.id === paymentLink.id)?.usages ?? []; const mixedToken = usagesForLink.some((u) => u.destinationToken.address !== paymentLink.destinationToken.address); return mixedToken ? "—" : /* formatted sum as above */;
309-312: Also invalidate “payment-link-usages” after deletionDeleting a link should refresh both the links list and the derived usages/revenue table so the counts and totals update immediately.
return queryClient.invalidateQueries({ queryKey: ["payment-links", props.clientId, props.teamId], }); + // Refresh usage-derived data as well + queryClient.invalidateQueries({ queryKey: ["payment-link-usages"], exact: false });apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/payments/page.tsx (1)
1-1: Follow dashboard UI import conventionsIn dashboard apps, import primitives from "@/components/ui/*" to keep design system usage consistent.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/RecentPaymentsSection.client.tsx (1)
111-115: Add rel to external link with target=_blankPrevents reverse tabnabbing and aligns with security best practices.
- <Link href="https://portal.thirdweb.com/payments" target="_blank"> + <Link href="https://portal.thirdweb.com/payments" target="_blank" rel="noreferrer"> View Documentation </Link>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx (1)
1-1: Use dashboard UI primitives from local pathFor consistency across dashboard, use "@/components/ui/button" rather than workspace-level import.
-import { Button } from "@workspace/ui/components/button"; +import { Button } from "@/components/ui/button";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx
Show resolved
Hide resolved
| page?: string; | ||
| }>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Type searchParams.page as string | string[] (Next.js pattern) to avoid surprises
searchParams params can be string | string[]. Narrow at the callsite and keep the type accurate here.
- page?: string;
+ page?: string | string[];📝 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.
| page?: string; | |
| }>; | |
| page?: string | string[]; | |
| }>; |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
around lines 22 to 23, the searchParams.page type is currently declared too
narrowly; Next.js can provide string | string[] for query params. Update the
type for page to be string | string[] (or widen searchParams to { page?: string
| string[] }) and then narrow it at the callsite (e.g., coerce to a single
string or handle arrays) so downstream code expects a string safely.
| <InAppWalletUsersPageContent | ||
| authToken={authToken} | ||
| client={client} | ||
| projectClientId={project.publishableKey} | ||
| teamId={project.teamId} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
High risk: client and/or authToken may be reaching client components
InAppWalletUsersPageContent is a client-side UI (uses hooks). Passing a secret-backed client or raw authToken risks leaking credentials. Move secret usage to server actions/internal API and pass only plain data or a public client (no jwt).
Suggested minimal direction:
- Remove client prop here; in the client component, create a browser-safe client via getClientThirdwebClient() without jwt solely for public reads (e.g., address formatting).
- Replace direct authToken usage in client UIs with internal API routes that read cookies server-side and return data.
- <InAppWalletUsersPageContent
- authToken={authToken}
- client={client}
+ <InAppWalletUsersPageContent
+ authToken={authToken} // preferably remove too; see note
projectClientId={project.publishableKey}
teamId={project.teamId}
/>I can draft a follow-up patch sequence if you want.
📝 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.
| <InAppWalletUsersPageContent | |
| authToken={authToken} | |
| client={client} | |
| projectClientId={project.publishableKey} | |
| teamId={project.teamId} | |
| /> | |
| <InAppWalletUsersPageContent | |
| authToken={authToken} // preferably remove too; see note | |
| projectClientId={project.publishableKey} | |
| teamId={project.teamId} | |
| /> |
| const client = getClientThirdwebClient({ | ||
| jwt: authToken, | ||
| teamId: project.teamId, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Do not pass a server-configured client (with JWT) into a Client Component.
CreateContractWebhookButton is a Client Component. Passing client created with jwt:
- Risks leaking server-only credentials to the browser.
- May fail RSC serialization if the client object contains functions/non-serializable state.
Prefer constructing a browser-safe client inside the Client Component using only the publishable key (clientId). Keep the server client on the server only.
Apply this minimal change here to stop passing the server client into the button:
<CreateContractWebhookButton
projectClientId={project.publishableKey}
supportedChainIds={supportedChainIds}
- client={client}
/>You will need to adjust the button component to create its own client on the client-side (example below).
Verification script to confirm assumptions:
Proposed client-side change (outside this file) for CreateWebhookModal.tsx:
// at top of the file
'use client';
// Replace the `client` prop with a safe publishable key (clientId)
export function CreateContractWebhookButton({
projectClientId,
supportedChainIds,
}: {
projectClientId: string;
supportedChainIds: number[];
}) {
// Construct a browser-safe client using publishable key only.
// Prefer the official thirdweb client factory for browser usage.
// Example (adjust to your util if available):
// const client = createThirdwebClient({ clientId: projectClientId });
// or if you have a helper:
// const client = getConfiguredThirdwebClient({ clientId: projectClientId });
// ...rest of the component unchanged, but use the locally created `client`
}Please verify which factory is canonical in this repo (e.g., createThirdwebClient vs a local getConfiguredThirdwebClient({ clientId })). If you want, I can push the follow-up patch across all call sites.
Also applies to: 41-47
🏁 Script executed:
#!/usr/bin/env bash
# 1) Ensure CreateWebhookModal is a Client Component and see its props
sed -n '1,30p' apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/CreateWebhookModal.tsx | nl -ba
rg -nC2 'export function CreateContractWebhookButton\(' apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/CreateWebhookModal.tsx
# 2) Check whether getClientThirdwebClient is flagged client-only or uses secrets
sed -n '1,60p' apps/dashboard/src/@/constants/thirdweb-client.client.ts | nl -ba
rg -n 'use client|secretKey|teamId' apps/dashboard/src/@/constants/thirdweb-client.client.tsLength of output: 320
🏁 Script executed:
#!/usr/bin/env bash
# 1) Locate the CreateContractWebhookButton component file and check for 'use client' directive
FILE_PATH=$(rg -l "export function CreateContractWebhookButton" --glob "apps/dashboard/src/app/**/webhooks/components/CreateWebhookModal.tsx")
echo "Component file: $FILE_PATH"
sed -n '1,30p' "$FILE_PATH" | nl -ba
rg -n "use client" "$FILE_PATH"
# 2) Locate the server-side client factory and inspect for secret usage
CLIENT_PATH=$(rg -l "getClientThirdwebClient" --glob "apps/dashboard/src/@/constants/*thirdweb-client.client.ts")
echo "Client factory file: $CLIENT_PATH"
sed -n '1,60p' "$CLIENT_PATH" | nl -ba
rg -n "secret" "$CLIENT_PATH"
rg -n "teamId" "$CLIENT_PATH"
rg -n "jwt" "$CLIENT_PATH"Length of output: 1843
🏁 Script executed:
#!/usr/bin/env bash
# Search for browser-side Thirdweb client factories
rg -n "createThirdwebClient" -g "apps/dashboard/src/**" || true
rg -n "getConfiguredThirdwebClient" -g "apps/dashboard/src/**" || true
rg -n "new ThirdwebClient" -g "apps/dashboard/src/**" | rg -v "getClientThirdwebClient" || trueLength of output: 9209
Fix client-side token leak in CreateContractWebhookButton
CreateContractWebhookButton is a Client Component (it has 'use client' at the top), but it currently receives a server-configured client built with getClientThirdwebClient({ jwt, teamId }). That factory injects secretKey and teamId (both server-only) into the client object, which
- Risks exposing secrets in the browser
- Breaks RSC serialization (non-serializable functions/state)
Instead, pass only the publishable key (project.publishableKey) into the component and instantiate a browser-safe client there with createThirdwebClient({ clientId }), which takes only publishable credentials.
• At the call site in page.tsx, remove the client prop:
- <CreateContractWebhookButton
- projectClientId={project.publishableKey}
- supportedChainIds={supportedChainIds}
- client={client}
- />
+ <CreateContractWebhookButton
+ projectClientId={project.publishableKey}
+ supportedChainIds={supportedChainIds}
+ />• In CreateWebhookModal.tsx, update the component to build its own client:
1 "use client";
2 import { createThirdwebClient } from "thirdweb";
// …
- export function CreateContractWebhookButton({
- projectClientId,
- supportedChainIds,
- }: {
- projectClientId: string;
- supportedChainIds: number[];
- }) {
- // old: expects client prop
- }
+ export function CreateContractWebhookButton({
+ projectClientId,
+ supportedChainIds,
+ }: {
+ projectClientId: string;
+ supportedChainIds: number[];
+ }) {
+ // Instantiate a browser-safe client using only the publishable key
+ const client = createThirdwebClient({ clientId: projectClientId });
+
+ // …rest unchanged, but now uses this local `client`
+ }This ensures no server-only secrets are passed into browser code and avoids serialization issues.
📝 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.
| const client = getClientThirdwebClient({ | |
| jwt: authToken, | |
| teamId: project.teamId, | |
| }); | |
| // File: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/page.tsx | |
| // …server-side setup remains unchanged | |
| const client = getClientThirdwebClient({ | |
| jwt: authToken, | |
| teamId: project.teamId, | |
| }); | |
| // …later in your JSX, drop the client prop | |
| <CreateContractWebhookButton | |
| projectClientId={project.publishableKey} | |
| supportedChainIds={supportedChainIds} | |
| /> |
| const client = getClientThirdwebClient({ | |
| jwt: authToken, | |
| teamId: project.teamId, | |
| }); | |
| // File: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/CreateWebhookModal.tsx | |
| "use client"; | |
| import React from "react"; | |
| import { createThirdwebClient } from "thirdweb"; | |
| // …other imports | |
| export function CreateContractWebhookButton({ | |
| projectClientId, | |
| supportedChainIds, | |
| }: { | |
| projectClientId: string; | |
| supportedChainIds: number[]; | |
| }) { | |
| // Instantiate a browser-safe client using only the publishable key | |
| const client = createThirdwebClient({ clientId: projectClientId }); | |
| // …rest of the component stays the same, now using this local `client` | |
| } |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/page.tsx
around lines 21-24, the server-built Thirdweb client created with
getClientThirdwebClient({ jwt, teamId }) is being passed into a Client
Component, which leaks server-only secrets and causes RSC serialization issues;
remove the client prop at the call site and pass only project.publishableKey (or
publishableKey as clientId) into the component, and in
CreateContractWebhookButton / CreateWebhookModal (the client component with 'use
client') instantiate a browser-safe client locally using createThirdwebClient({
clientId: project.publishableKey }) so no jwt/teamId/secretKey is sent to the
browser and the component manages its own publishable-only client.

PR-Codex overview
This PR primarily focuses on the removal of various files related to the
analytics,webhooks, andpaymentsfeatures in the application, alongside several layout and component updates for improved structure and usability.Detailed summary
analytics,webhooks, andpayments.Summary by CodeRabbit