Skip to content

Conversation

@jnsdls
Copy link
Member

@jnsdls jnsdls commented Aug 20, 2025

PR-Codex overview

This PR primarily focuses on the removal of various files related to the analytics, webhooks, and payments features in the application, alongside several layout and component updates for improved structure and usability.

Detailed summary

  • Deleted multiple files related to analytics, webhooks, and payments.
  • Updated various components to enhance layout and styling.
  • Refactored several functions and types for better clarity and maintainability.
  • Introduced new components and modified existing ones for improved user experience.
  • Adjusted import paths and component structures for consistency.
  • Enhanced project page layouts across different features.

The following files were skipped due to too many changes: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx, apps/dashboard/src/@/components/blocks/project-page/project-page-header.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(general)/import/import-engine-dialog.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx, pnpm-lock.yaml

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

Summary by CodeRabbit

  • New Features
    • ProjectPage wrapper for consistent header/footer across many sections; new import/subscription actions (ImportEngineButton, DedicatedEngineSubscriptionButton, ImportContractButton, ImportTokenButton); payments: Create Payment Link, Payment Links table; server wallets table with pagination.
  • Enhancements
    • Unified headers/actions/tabs, icon and layout polish, sidebar shows Engine only when instances exist, simplified Webhooks routing.
  • Removed/Deprecated
    • Many legacy pages/components for Webhooks (analytics, configs, modals), Payments analytics/routes, Engine import/legacy pages, API Explorer/Scalar, various loading wrappers.
  • Chores
    • Removed PostHog dependency/feature-flag server code and other unused deps.

@vercel
Copy link

vercel bot commented Aug 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
docs-v2 Ready Ready Preview Comment Aug 20, 2025 11:24pm
nebula Ready Ready Preview Comment Aug 20, 2025 11:24pm
thirdweb_playground Ready Ready Preview Comment Aug 20, 2025 11:24pm
thirdweb-www Ready Ready Preview Comment Aug 20, 2025 11:24pm
wallet-ui Ready Ready Preview Comment Aug 20, 2025 11:24pm

@changeset-bot
Copy link

changeset-bot bot commented Aug 20, 2025

⚠️ No Changeset found

Latest commit: a5e97e2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

Walkthrough

Replaces 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

Cohort / File(s) Summary
Dependencies
apps/dashboard/package.json
Removed @scalar/api-reference-react and posthog-node.
Analytics & feature-flag removal
apps/dashboard/src/@/analytics/posthog-server.ts, apps/dashboard/src/@/types/analytics.ts, apps/dashboard/src/@/api/analytics.ts
Deleted server PostHog helper and isFeatureFlagEnabled; removed WebhookRequestStats type and webhook analytics endpoints/exports.
Webhook configs API & UI removals
apps/dashboard/src/@/api/project/webhook-configs.ts, apps/dashboard/src/app/.../webhooks/**
Removed webhook-config client, analytics pages, charts, pickers, topic selector, modals (create/edit/delete), configs table, metrics, and many webhooks layouts/pages/loaders. Some webhooks routes now redirect to contracts webhooks.
ProjectPage building blocks (new)
apps/dashboard/src/@/components/blocks/project-page/*, .../header/link-group.tsx
Added ProjectPage layout, ProjectPageHeader/Footer components, LinkGroup and ActionLink type for header actions/links.
Sidebar & layout integration
apps/dashboard/src/app/.../(sidebar)/layout.tsx, .../components/ProjectSidebarLayout.tsx, .../hooks/useEngine.ts, .../components/ui/tabs.tsx
Simplified sidebar props to hasEngineInstances, removed feature-flag checks, updated engine instance imports, introduced TabPathLink type and TabPathLinks prop change.
Account Abstraction
apps/dashboard/src/app/.../account-abstraction/*
Migrated main page to ProjectPage, added server AA settings page, moved settings header, deleted legacy layout/footer files; exported YourFactoriesSection.
Engine
apps/dashboard/src/app/.../engine/**
Wrapped Engine page with ProjectPage; added ImportEngineButton dialog; exported DedicatedEngineSubscriptionButton; removed legacy dedicated import pages, layouts, loaders, and stories.
Contracts
apps/dashboard/src/app/.../contracts/*
Converted Contracts page to ProjectPage with Deploy/Import actions; added ImportContractButton; removed legacy contracts layout/header and contracts webhooks page.
Tokens
apps/dashboard/src/app/.../tokens/*
Tokens page moved into ProjectPage; added ImportTokenButton, refactored Cards API and token create/import flows.
Payments
apps/dashboard/src/app/.../payments/**
Consolidated Payments under ProjectPage; refactored QuickStart/FeatureCard APIs, removed BuyWidgetFTUX and code-examples, changed RecentPaymentsSection to accept jwt and construct client internally, refactored links table, removed legacy payments layouts/pages/loaders.
Payments Webhooks
apps/dashboard/src/app/.../webhooks/payments/page.tsx, .../payments/webhooks/components/webhooks.client.tsx
Added Payments Webhooks ProjectPage; introduced CreatePaymentWebhookButton (encapsulates create flow) and styling tweaks.
RPC, Vault
apps/dashboard/src/app/.../rpc/page.tsx, .../vault/page.tsx (deleted layouts)
Wrapped RPC and Vault pages in ProjectPage; removed corresponding legacy layout files.
Wallets
apps/dashboard/src/app/.../wallets/*
Wallets page migrated to ProjectPage with client header; removed separate wallets layouts, header/footer components, analytics redirect pages and loaders; users page now redirects to wallets.
Transactions
apps/dashboard/src/app/.../transactions/*
Transactions page wrapped with ProjectPage; added server-wallets pagination and ServerWalletsTable usage; removed explorer, dedicated transactions layouts and server-wallets page; minor UI/header icon adjustments.
Various UI tweaks
apps/dashboard/src/@/components/analytics/responsive-time-filters.tsx, .../full-width-sidebar-layout.tsx, .../contracts/import-contract/modal.tsx, .../DeployedContractsPage.tsx, .../tx-table-ui.tsx
ClassName, spacing, and icon changes; removed some container constraints and adjusted button/icon presentation.
Loads/redirects removed
apps/dashboard/src/app/.../connect/analytics/*, many loading.tsx files across routes
Deleted multiple loader stubs and simple redirect pages; several routes now use ProjectPage or redirect to other subroutes.

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
Loading
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
Loading

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 08-20-unified_project_layout

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the Dashboard Involves changes to the Dashboard. label Aug 20, 2025
@jnsdls jnsdls marked this pull request as ready for review August 20, 2025 10:00
Copy link
Member Author

jnsdls commented Aug 20, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge-queue - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

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.

@codecov
Copy link

codecov bot commented Aug 20, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.53%. Comparing base (8b6dead) to head (a5e97e2).
⚠️ Report is 1 commits behind head on main.

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              
Flag Coverage Δ
packages 56.53% <ø> (-0.01%) ⬇️
see 2 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 20, 2025

size-limit report 📦

Path Size Loading time (3g) Running time (snapdragon) Total time
thirdweb (esm) 64.06 KB (0%) 1.3 s (0%) 216 ms (+43.15% 🔺) 1.5 s
thirdweb (cjs) 357.05 KB (0%) 7.2 s (0%) 1.3 s (+2.55% 🔺) 8.4 s
thirdweb (minimal + tree-shaking) 5.73 KB (0%) 115 ms (0%) 57 ms (+501.14% 🔺) 171 ms
thirdweb/chains (tree-shaking) 526 B (0%) 11 ms (0%) 32 ms (+825.49% 🔺) 43 ms
thirdweb/react (minimal + tree-shaking) 19.15 KB (0%) 383 ms (0%) 45 ms (+123.98% 🔺) 428 ms

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 entries

serviceToAdd 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 image

If 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 warning

Minor 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-fns

The Universal Bridge client returns createdAt as a string (ISO), but formatDistanceToNow expects a Date or 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–113

Diff:

- {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 via clientThirdwebClient

The getClientThirdwebClient helper (apps/dashboard/src/@/constants/thirdweb-client.client.ts) passes your JWT (secretKey) into a ThirdwebClient instance. That server-authenticated client is then sent into the browser via the clientThirdwebClient prop 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 queries

useWalletBalance (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 via getClientThirdwebClient

The helper in thirdweb-client.client.ts simply forwards your jwt as secretKey into a ThirdwebClient instance, 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 passing getClientThirdwebClient({ jwt, … }) into client-side code

Please 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 link

The 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 Link
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx (2)

36-38: Add an early return after loginRedirect to halt execution

In 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 Component

The getClientThirdwebClient helper lives in apps/dashboard/src/@/constants/thirdweb-client.client.ts, which Next.js will treat as a client module (due to the .client.ts extension). Importing a client module into a server component (page.tsx) will break the build.

Please refactor as follows:

  • Rename thirdweb-client.client.ts to either:
    • thirdweb-client.server.ts
    • or drop the suffix entirely (thirdweb-client.ts)
  • Update all imports of getClientThirdwebClient accordingly.
  • Optionally, if you need a client‐side variant later, keep that in a separate *.client.ts helper; keep your server code in the .server.ts file.

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 scale

Per 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.ReactNode inline. 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 gracefully

Using the presence of engine instances to gate the UI is the right call. On fetch failure, hasEngineInstances becomes 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-only

Per 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 alias

Matches 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 selected

Prevents 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 punctuation

Non-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 call

serviceDefinition 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 guidelines

Add 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-invalidation

Current 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 consistency

Mirror 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 delete

You 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” consistently

Column 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 handling

form.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 button

In 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 explicitly

This 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 UnderlineLink

The 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 button

The 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 explicitly

This 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 path

Ensure 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 type

Exported 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 readability

Minor 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 contracts

Fetching 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 handlers

Currently the Dialog is controlled via open={isOpen}, but there’s no onOpenChange, and the Trigger button also calls handleOpenModal. This prevents overlay/Escape from closing the dialog and can double-fire open logic. Suggest controlling via onOpenChange and 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.ok guard 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 stable

Returning null removes 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 explicitly

This 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 mobile

Previously 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 redundancy

Using 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” context

Text 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 target

The 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 wrapper

This 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_getWebhookLatency is dead code—there’s no getWebhookLatency export to invoke it. You have two options:

  • Remove the cache and drop WebhookLatencyStats from 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 alias

Per 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 efficiency

Fetching 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 functions

TypeScript 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 colors

Replace 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 prop

color 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 path

Align 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 use

Cards 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 import

Swap 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 module

Importing types from deep page component paths couples this hook to the app routing layer and makes refactors brittle. Prefer re-exporting or relocating ResultItem and EngineStatus to a shared types barrel (e.g., @/types/engine or a local types.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 page

To 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 boundaries

This 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 function

Per 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 coupling

This 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: Add server-only directive for server component safety

This 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 function

Our 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 selected

The 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 gracefully

An empty tabs array currently renders an empty Tab bar instead of the Separator. Check length before rendering.

-      {props.tabs ? (
+      {props.tabs?.length ? (
         <TabPathLinks
           className="-mt-8"
           tabContainerClassName="container"
           links={props.tabs}
         />
       ) : (
         <Separator />
       )}

22-24: Add explicit return type

Per 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 clarity

Define 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: Add server-only for this redirector page

This is a server-only redirector; add the directive at the top.

+import "server-only";
 import { redirect } from "next/navigation";

3-6: Optional: use conventional Page export name for consistency

Across 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 URLSearchParams

Directly interpolating searchParams.importUrl into 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 undefined

Because the early redirect (Line 26) triggers whenever importUrl exists, prefillImportUrl={searchParams.importUrl} will never receive a value on this page. Decide one of:

  • Keep redirect for legacy deep links, and drop prefillImportUrl here.
  • 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.error is 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 variable

You define const defaultRange: DurationId = "last-30"; but pass the string literal again to getFiltersFromSearchParams. 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: Verify Header usage and cleanup
It appears the Header function 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 export keyword so it’s a private helper.
• If it’s intended for reuse, move it into a shared components/ folder and update the import paths.
• Otherwise, delete the entire Header definition 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 URLs

If 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.error assumes a specific shape. If apiServerProxy returns a different error layout, this may throw a confusing error. Consider typing apiServerProxy response here and guarding res.error existence 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 mutation

Prevent 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 colors

The 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 bundling

These 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-only

This 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-only

This 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 contract

PayAnalytics 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",
             },

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 the href to
    ${props.layoutPath}/webhooks/payments.

  • There is no insight or insights page 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
      (or insight/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 component

Ensure 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 execution

Prevent 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 execution

  if (!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 pages

Number.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 paths

TransactionsAnalyticsPageContent 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 Barrel

I’ve confirmed that both ResultItem and EngineStatus are correctly exported from their respective component files, so you can safely decouple useEngine.ts from 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 key

Include 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 consistency

In 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 Button

Stay 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 Page

For 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> {
  // ...
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 props

The page.tsx under
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.tsx starts with "use client" (a client component) and its props signature doesn’t even include client. 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:
    1. Sensitive calls using the JWT happen server-side (e.g. via a server action or API route using getAuthToken() and getClientThirdwebClient()), returning only serializable data.
    2. The client component (aa-analytics.tsx) instantiates its own ThirdwebClient without a JWT (public config only) and calls your internal endpoints (using a fetchJson wrapper 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 components

I ran the verification script across all "use client" components in the transactions subtree and found these files still declaring a client prop typed as ThirdwebClient, 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: ThirdwebClient props and instead instantiate or retrieve the vault client within the component (for example, via createVaultClient) 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 component

You'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 only clientId), 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 ≥ 1

Number.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 suppression

The 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 tables

TransactionsAnalyticsPageContent 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_getWebhookLatency appears to be dead code after the webhook analytics public APIs were removed. It also keeps WebhookLatencyStats imported and introduces an error shape inconsistent with the rest of this module.

Action:

  • Delete this block and remove WebhookLatencyStats from 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_getWebhookSummary and _cached_getWebhookLatency are 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 the errorMessage pattern elsewhere.

• Delete both cache blocks (lines ~662–688 and ~690–716 in apps/dashboard/src/@/api/analytics.ts).
• Drop WebhookSummaryStats and WebhookLatencyStats from 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 explicit Promise<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.tsx exists and exports a default ContractsPage.
  • 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 of redirect() 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/engine

Type-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 guidelines

Most 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 path

Per 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 directive

Server 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 Page

Our 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 fees

Currently you default in both catch and an if (!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 DRY

Minor clean-up; avoids calling getLastNDaysRange twice 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 searchParamLoader guarantees from/to when rangeType === "custom"; otherwise add a fallback.


88-92: Clarify billing gating variable and verify status semantics

Rename for readability and confirm whether pastDue should also prompt an alert (currently excluded).

-  const hasSmartWalletsWithoutBilling =
+  const shouldShowBillingAlert =
     isBundlerServiceEnabled &&
     team.billingStatus !== "validPayment" &&
     team.billingStatus !== "pastDue";
@@
-      {hasSmartWalletsWithoutBilling && (
+      {shouldShowBillingAlert && (

Follow-up: If pastDue should 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 spacing

Avoid 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 case

If 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 too

Carry 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 styling

For 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 values

Use 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 functions

Add 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 execution

loginRedirect 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 guidelines

Dashboard 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 primary

Since 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} />

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 components

The authToken prop 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 all CustomChatButton usages 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 authToken into CustomChatButton.
  • Inside CustomChatButton (client component), call a server action or hit an internal API route (e.g. /api/chat/token) that uses getAuthToken() 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 authToken directly 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 client

After 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 fallthrough

Per 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 yield NaN (e.g., ?page=foo). Also, searchParams.page can 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-error

The SDK expects pageSize (camelCase). Also clamp page to >= 0 to avoid negative pages when currentPage === 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-created ThirdwebClient or managementAccessToken to a client component (EngineChecklist)

EngineChecklist is a client component (ftux.client.tsx). Passing client (configured with JWT) or managementAccessToken crosses 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 client or managementAccessToken prop):

#!/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 server client into TransactionsAnalyticsPageContent (it fans out to client tables)

TransactionsAnalyticsPageContent is server, but it forwards client to TransactionsTable, which is likely client. Remove client from 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 client from 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 client is 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 pass managementAccessToken or server client to ServerWalletsTable (and its UI is client-side)

ServerWalletsTableUI is a client component; passing managementAccessToken or a JWT-configured client leaks 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 needs managementAccessToken. 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*:' {} || true
apps/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 booleans

Use 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 bundle

In 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 UI

Import 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 components

Header 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 ContractTable

Keep 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 indirectly

You 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 header

As 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 props

This 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 browser

getClientThirdwebClient({ 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 header

Passing 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-side client or authToken into client components

We’ve confirmed that in
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PayAnalytics.tsx
the server-only PayAnalytics is 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 in PaymentHistory.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:

  1. Move all data fetching (using client and authToken) into a server action or internal API route.
  2. Return only plain JSON data to the client.
  3. Update PaymentHistory and PayCustomersTable to accept sanitized data props (e.g. historyData, customerData) instead of client/authToken.
  4. 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: Add import "server-only"; to assert server component boundary

This 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 safety

Explicit 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 useIsSubnavActive

Keeps 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 Behavior

AppFooter’s implementation always wraps its inner content in a <div> with the container class (and associated max-width breakpoints), regardless of whether you pass containerClassName. 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 override containerClassName, 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?: string is optional, and by default AppFooter prepends "container flex …" to its inner wrapper.
    • No usages of containerClassName found 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 explicitly

Add 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 shapes

Matches 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 Page

Be 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 getLastNDaysRange

Compute 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 pastDue

Current 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 divs

Prefer 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 handling

You 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.ts and does not import or call next/headers; it relies only on window.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.Element

Also 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 on project; fetch both concurrently after resolving params and authToken.

-  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 links point to the same docs URL. Keep one to reduce UI noise; I recommend keeping the secondary action button and dropping the redundant links item.

         },
-        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 component

Project 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 fetches

When 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 feature

This 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 deletion

Deleting 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 conventions

In 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=_blank

Prevents 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 path

For 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";

Comment on lines +22 to 23
page?: string;
}>;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

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.

Suggested change
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.

Comment on lines +111 to +116
<InAppWalletUsersPageContent
authToken={authToken}
client={client}
projectClientId={project.publishableKey}
teamId={project.teamId}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
<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}
/>

Comment on lines +21 to +24
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
Copy link
Contributor

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.ts

Length 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" || true

Length 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.

Suggested change
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}
/>
Suggested change
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.

@jnsdls jnsdls merged commit a02b557 into main Aug 21, 2025
24 checks passed
@jnsdls jnsdls deleted the 08-20-unified_project_layout branch August 21, 2025 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Dashboard Involves changes to the Dashboard.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants