-
Notifications
You must be signed in to change notification settings - Fork 615
[WIP] Refactor wallet and sidebar routes, add new wallet pages #8359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Reorganized wallet-related routes under a new sidebar structure, replacing legacy pages with redirects to new user, server, and sponsored gas wallet sections. Added new layout and overview/configuration pages for server wallets and sponsored gas, updated sidebar navigation to reflect new wallet grouping, and removed legacy Account Abstraction and Vault pages in favor of the new structure.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. |
WalkthroughThis PR restructures wallet management and project routing by replacing old account abstraction, vault, and transactions pages with redirects to new wallet subsections. It introduces new layout, overview, and configuration pages for user-wallets, server-wallets, and sponsored-gas features, alongside updated sidebar navigation and in-app wallet settings UI. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Browser
participant Dashboard as Dashboard Page
participant Auth as Auth Service
participant Project as Project Service
participant Wallet as Wallet Service
User->>Browser: Navigate to /wallets
Browser->>Dashboard: Request /wallets/user-wallets/layout
Dashboard->>Dashboard: Resolve params
Dashboard->>Auth: getAuthToken()
Dashboard->>Project: getProject(team_slug, project_slug)
alt Auth missing
Dashboard->>User: Redirect to login
end
alt Project missing
Dashboard->>User: Redirect to team page
end
Dashboard->>Dashboard: Create thirdweb client
Dashboard->>Browser: Render TabPathLinks (Overview, Configuration)
Note over Browser: User on Overview tab
Dashboard->>Auth: getAuthToken()
Dashboard->>Project: getProject() + getTeamBySlug()
Dashboard->>Wallet: Fetch wallet analytics & users
Dashboard->>Browser: Render InAppWalletsSummary, InAppWalletAnalytics
Note over Browser: User navigates to Configuration
Dashboard->>Dashboard: getValidTeamPlan(), getSMSCountryTiers()
Dashboard->>Browser: Render InAppWalletSettingsUI (plan-gated)
User->>Browser: Update branding/auth settings
Browser->>Dashboard: updateProjectClient mutation
Dashboard->>Project: Update project.services.embeddedWallets
Dashboard->>User: Show success toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
size-limit report 📦
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx (1)
121-159: Restore custom active matching for sidebar links
ShadcnSidebarBaseLinkexposes anisActivehook so callers can override the matching logic. The newwalkhelper no longer consults that callback, so any link relying on custom matching (or non-prefix URLs) stops lighting up both in the sidebar and mobile trigger. Please wire the callback back in before falling back to the default comparisons.- function isActive(link: ShadcnSidebarBaseLink) { - if (link.exactMatch) { - return link.href === pathname; - } - return pathname?.startsWith(link.href); - } + function isActive(link: ShadcnSidebarBaseLink) { + const currentPath = pathname ?? ""; + if (link.isActive) { + return link.isActive(currentPath); + } + if (link.exactMatch) { + return link.href === currentPath; + } + return currentPath.startsWith(link.href); + }
🧹 Nitpick comments (2)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx (1)
1-11: Clean redirect pattern, but consider adding server-only directive.The refactor from a complex page to a simple redirect is well-executed and follows the pattern established in other wallet redirect pages in this PR. The target page (
wallets/server-wallets/overview/page.tsx) properly handles the full implementation including authorization, data fetching, and UI rendering.However, per coding guidelines, server components should start with
import "server-only"to enforce server-side-only execution. This applies to all similar redirect pages in this PR (vault, account-abstraction, wallets, server-wallets).Consider adding the directive at the top of the file:
+import "server-only"; + import { redirect } from "next/navigation";Note: This same pattern should be applied to the other redirect pages in this PR for consistency with the coding guidelines.
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/layout.tsx (1)
19-23: Consider using a constant or helper for the redirect path.The hardcoded redirect path
/team/${params.team_slug}/${params.project_slug}/wallets/user-walletscould be extracted to a constant or helper function to improve maintainability and reduce duplication if this path is used elsewhere in the wallets section.
| @@ -0,0 +1,62 @@ | |||
| import { redirect } from "next/navigation"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add "server-only" import at the top.
This server component uses server-side APIs and should begin with import "server-only"; to prevent accidental client-side bundling.
As per coding guidelines
Apply this diff:
+import "server-only";
+
import { redirect } from "next/navigation";🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/layout.tsx
at line 1, this server component must include the server-only marker to avoid
client-side bundling; add the statement import "server-only"; as the very first
line of the file (before any other imports) so Next treats this module as
server-only.
| import { redirect } from "next/navigation"; | ||
| import { getAuthToken } from "@/api/auth-token"; | ||
| import { getProject } from "@/api/project/projects"; | ||
| import { loginRedirect } from "@/utils/redirects"; | ||
| import { KeyManagement } from "../../../vault/components/key-management"; | ||
|
|
||
| export default async function Page(props: { | ||
| params: Promise<{ team_slug: string; project_slug: string }>; | ||
| }) { | ||
| const { team_slug, project_slug } = await props.params; | ||
| const [authToken, project] = await Promise.all([ | ||
| getAuthToken(), | ||
| getProject(team_slug, project_slug), | ||
| ]); | ||
|
|
||
| if (!authToken) { | ||
| loginRedirect( | ||
| `/team/${team_slug}/${project_slug}/wallets/server-wallets/configuration`, | ||
| ); | ||
| } | ||
|
|
||
| if (!project) { | ||
| redirect(`/team/${team_slug}`); | ||
| } | ||
|
|
||
| const projectEngineCloudService = project.services.find( | ||
| (service) => service.name === "engineCloud", | ||
| ); | ||
|
|
||
| const maskedAdminKey = projectEngineCloudService?.maskedAdminKey; | ||
| const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey; | ||
|
|
||
| return ( | ||
| <KeyManagement | ||
| maskedAdminKey={maskedAdminKey ?? undefined} | ||
| isManagedVault={isManagedVault} | ||
| project={project} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add the server-only guard
This page executes on the server (auth token fetch, redirects, etc.), so it should start with import "server-only"; to keep it out of client bundles and align with our dashboard rules. As per coding guidelines.
+import "server-only";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";📝 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.
| import { redirect } from "next/navigation"; | |
| import { getAuthToken } from "@/api/auth-token"; | |
| import { getProject } from "@/api/project/projects"; | |
| import { loginRedirect } from "@/utils/redirects"; | |
| import { KeyManagement } from "../../../vault/components/key-management"; | |
| export default async function Page(props: { | |
| params: Promise<{ team_slug: string; project_slug: string }>; | |
| }) { | |
| const { team_slug, project_slug } = await props.params; | |
| const [authToken, project] = await Promise.all([ | |
| getAuthToken(), | |
| getProject(team_slug, project_slug), | |
| ]); | |
| if (!authToken) { | |
| loginRedirect( | |
| `/team/${team_slug}/${project_slug}/wallets/server-wallets/configuration`, | |
| ); | |
| } | |
| if (!project) { | |
| redirect(`/team/${team_slug}`); | |
| } | |
| const projectEngineCloudService = project.services.find( | |
| (service) => service.name === "engineCloud", | |
| ); | |
| const maskedAdminKey = projectEngineCloudService?.maskedAdminKey; | |
| const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey; | |
| return ( | |
| <KeyManagement | |
| maskedAdminKey={maskedAdminKey ?? undefined} | |
| isManagedVault={isManagedVault} | |
| project={project} | |
| /> | |
| ); | |
| } | |
| import "server-only"; | |
| import { redirect } from "next/navigation"; | |
| import { getAuthToken } from "@/api/auth-token"; | |
| import { getProject } from "@/api/project/projects"; | |
| import { loginRedirect } from "@/utils/redirects"; | |
| import { KeyManagement } from "../../../vault/components/key-management"; | |
| export default async function Page(props: { | |
| params: Promise<{ team_slug: string; project_slug: string }>; | |
| }) { | |
| const { team_slug, project_slug } = await props.params; | |
| const [authToken, project] = await Promise.all([ | |
| getAuthToken(), | |
| getProject(team_slug, project_slug), | |
| ]); | |
| if (!authToken) { | |
| loginRedirect( | |
| `/team/${team_slug}/${project_slug}/wallets/server-wallets/configuration`, | |
| ); | |
| } | |
| if (!project) { | |
| redirect(`/team/${team_slug}`); | |
| } | |
| const projectEngineCloudService = project.services.find( | |
| (service) => service.name === "engineCloud", | |
| ); | |
| const maskedAdminKey = projectEngineCloudService?.maskedAdminKey; | |
| const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey; | |
| return ( | |
| <KeyManagement | |
| maskedAdminKey={maskedAdminKey ?? undefined} | |
| isManagedVault={isManagedVault} | |
| project={project} | |
| /> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/configuration/page.tsx
lines 1-40, this server-rendered page must be explicitly marked server-only; add
import "server-only"; as the very first line of the file (before any other
imports) to ensure it is excluded from client bundles and follows the dashboard
server-only rule.
| if (!authToken) { | ||
| redirect( | ||
| `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the auth redirect loop.
When authToken is missing, this code redirects back to the same /wallets/server-wallets/overview path, which will immediately re-run the page loader and redirect again—effectively an infinite loop that surfaces as a 500. Redirect to a safe auth entry point instead (e.g. /login with a return URL) so unauthenticated users can recover.
- if (!authToken) {
- redirect(
- `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`,
- );
- }
+ if (!authToken) {
+ redirect(`/login?next=/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`);
+ }📝 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.
| if (!authToken) { | |
| redirect( | |
| `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`, | |
| ); | |
| } | |
| if (!authToken) { | |
| redirect(`/login?next=/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`); | |
| } |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/overview/page.tsx
at lines 36 to 40, the redirect when authToken is missing points back to the
same page path, creating an infinite redirect loop that causes a 500 error.
Replace the redirect destination with a login page endpoint and include a
returnUrl query parameter that points back to the original wallets overview page
so users can be redirected there after successful authentication. This allows
unauthenticated users to properly authenticate rather than getting stuck in a
loop.
| if (!authToken) { | ||
| notFound(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use loginRedirect instead of notFound for auth guard
If the auth token is missing we should redirect the user to the login flow, not surface a 404. The current notFound() call will show the Not Found page to logged-out users, which is inconsistent with the rest of the dashboard wallet routes and breaks the intended authentication UX. Please swap this for loginRedirect(...) (and drop the notFound import).
-import { notFound, redirect } from "next/navigation";
+import { redirect } from "next/navigation";
+import { loginRedirect } from "@/utils/redirects";
@@
- if (!authToken) {
- notFound();
- }
+ if (!authToken) {
+ loginRedirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas/overview`,
+ );
+ }🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/overview/page.tsx
around lines 29-31, replace the current auth guard that calls notFound() when
authToken is missing with a loginRedirect call so unauthenticated users are sent
to the login flow; remove the notFound import, import the loginRedirect helper
instead, and invoke it passing the current request URL as the callback (e.g.
loginRedirect({ callbackUrl: currentUrl })) or otherwise ensure the user is
redirected back to this page after login.
| export async function getSMSCountryTiers() { | ||
| if (!API_SERVER_SECRET) { | ||
| throw new Error("API_SERVER_SECRET is not set"); | ||
| } | ||
| const res = await fetch( | ||
| `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/sms/list-country-tiers`, | ||
| { | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-service-api-key": API_SERVER_SECRET, | ||
| }, | ||
| next: { | ||
| revalidate: 15 * 60, //15 minutes | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (!res.ok) { | ||
| console.error( | ||
| "Failed to fetch sms country tiers", | ||
| res.status, | ||
| res.statusText, | ||
| ); | ||
| res.body?.cancel(); | ||
| return { | ||
| tier1: [], | ||
| tier2: [], | ||
| tier3: [], | ||
| tier4: [], | ||
| tier5: [], | ||
| tier6: [], | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| return (await res.json()).data as SMSCountryTiers; | ||
| } catch (e) { | ||
| console.error("Failed to parse sms country tiers", e); | ||
| return { | ||
| tier1: [], | ||
| tier2: [], | ||
| tier3: [], | ||
| tier4: [], | ||
| tier5: [], | ||
| tier6: [], | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Declare the return type for getSMSCountryTiers
Our TypeScript guidelines require explicit return types for exported functions; without it we lose the compile-time contract that this helper always resolves to SMSCountryTiers. Please add the annotation.
-export async function getSMSCountryTiers() {
+export async function getSMSCountryTiers(): Promise<SMSCountryTiers> {🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/api/sms.ts
around lines 14 to 61, the exported async function getSMSCountryTiers is missing
an explicit return type; add an explicit return annotation
(Promise<SMSCountryTiers>) to the function signature and ensure SMSCountryTiers
is imported or declared in scope so the compiler can validate the returned
shape; no other logic changes are required but keep the existing fallback return
objects matching the SMSCountryTiers interface.
| import { redirect } from "next/navigation"; | ||
|
|
||
| export default async function Page(props: { | ||
| params: Promise<{ team_slug: string; project_slug: string }>; | ||
| }) { | ||
| const params = await props.params; | ||
| redirect( | ||
| `/team/${params.team_slug}/${params.project_slug}/wallets/user-wallets/overview`, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add the server-only guard
This route runs entirely on the server; per our dashboard conventions we need import "server-only"; at the top so bundlers never treat it as a client component. Please add the guard. As per coding guidelines.
+import "server-only";
import { redirect } from "next/navigation";🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/page.tsx
around lines 1 to 9, this route is server-only and needs the server-only guard;
add the statement import "server-only"; as the first line (above any other
imports) so bundlers treat the module as server-only per dashboard conventions.
Reorganized wallet-related routes under a new sidebar structure, replacing legacy pages with redirects to new user, server, and sponsored gas wallet sections. Added new layout and overview/configuration pages for server wallets and sponsored gas, updated sidebar navigation to reflect new wallet grouping, and removed legacy Account Abstraction and Vault pages in favor of the new structure.
PR-Codex overview
This PR focuses on enhancing the wallet management feature within the dashboard by introducing new components for country selection in SMS configurations, improving navigation, and redirecting old pages to new ones.
Detailed summary
CountrySelectorcomponent for SMS country selection.user-wallets,sponsored-gas, andserver-wallets.Summary by CodeRabbit
New Features
Refactor