diff --git a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx
index 0054ac769cc..7f4bee84f78 100644
--- a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx
+++ b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx
@@ -30,7 +30,7 @@ import {
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
-type ShadcnSidebarBaseLink = {
+export type ShadcnSidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
@@ -38,11 +38,11 @@ type ShadcnSidebarBaseLink = {
isActive?: (pathname: string) => boolean;
};
-type ShadcnSidebarLink =
+export type ShadcnSidebarLink =
| ShadcnSidebarBaseLink
| {
group: string;
- links: ShadcnSidebarBaseLink[];
+ links: ShadcnSidebarLink[];
}
| {
separator: true;
@@ -97,10 +97,7 @@ export function FullWidthSidebarLayout(props: {
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
const activeLink = useActiveShadcnSidebarLink(props.links);
- const parentSubNav = props.links.find(
- (link) =>
- "subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
- );
+ const parentSubNav = findParentSubmenu(props.links, activeLink?.href);
return (
@@ -109,7 +106,7 @@ function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
className="h-4 bg-muted-foreground/50"
orientation="vertical"
/>
- {parentSubNav && "subMenu" in parentSubNav && (
+ {parentSubNav && (
<>
{parentSubNav.subMenu.label}
@@ -131,24 +128,65 @@ function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
return pathname?.startsWith(link.href);
}
- for (const link of links) {
- if ("links" in link) {
- for (const subLink of link.links) {
- if (isActive(subLink)) {
- return subLink;
+ function walk(
+ navLinks: ShadcnSidebarLink[],
+ ): ShadcnSidebarBaseLink | undefined {
+ for (const link of navLinks) {
+ if ("subMenu" in link) {
+ for (const subLink of link.links) {
+ if (isActive(subLink)) {
+ return subLink;
+ }
+ }
+ } else if ("href" in link) {
+ if (isActive(link)) {
+ return link;
}
}
- } else if ("href" in link) {
- if (isActive(link)) {
- return link;
+
+ if ("links" in link && !("subMenu" in link)) {
+ const nested = walk(link.links);
+ if (nested) {
+ return nested;
+ }
}
}
+
+ return undefined;
}
+
+ return walk(links);
}, [links, pathname]);
return activeLink;
}
+function findParentSubmenu(
+ links: ShadcnSidebarLink[],
+ activeHref: string | undefined,
+): Extract
| undefined {
+ if (!activeHref) {
+ return undefined;
+ }
+
+ for (const link of links) {
+ if ("subMenu" in link) {
+ if (link.links.some((subLink) => subLink.href === activeHref)) {
+ return link;
+ }
+ }
+
+ if ("links" in link && !("subMenu" in link)) {
+ const nested = findParentSubmenu(link.links, activeHref);
+ if (nested) {
+ return nested;
+ }
+ }
+ }
+
+ return undefined;
+}
+
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
const pathname = usePathname();
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx
index 6ca23d1d256..c38b2ebe90b 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx
@@ -1,163 +1,11 @@
-import type { Metadata } from "next";
-import { notFound, redirect } from "next/navigation";
-import type { SearchParams } from "nuqs/server";
-import { getUserOpUsage } from "@/api/analytics";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import { getTeamBySlug } from "@/api/team/get-team";
-import {
- getLastNDaysRange,
- type Range,
-} from "@/components/analytics/date-range-selector";
-import { ProjectPage } from "@/components/blocks/project-page/project-page";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
-import { getAbsoluteUrl } from "@/utils/vercel";
-import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary";
-import { SmartWalletsBillingAlert } from "./Alerts";
-import { AccountAbstractionAnalytics } from "./aa-analytics";
-import { searchParamLoader } from "./search-params";
-
-interface PageParams {
- team_slug: string;
- project_slug: string;
-}
+import { redirect } from "next/navigation";
+// Redirect old Account Abstraction page to new Sponsored Gas Overview
export default async function Page(props: {
- params: Promise;
- searchParams: Promise;
- children: React.ReactNode;
+ params: Promise<{ team_slug: string; project_slug: string }>;
}) {
- const [params, searchParams, authToken] = await Promise.all([
- props.params,
- searchParamLoader(props.searchParams),
- getAuthToken(),
- ]);
-
- if (!authToken) {
- notFound();
- }
-
- const [team, project] = await Promise.all([
- getTeamBySlug(params.team_slug),
- getProject(params.team_slug, params.project_slug),
- ]);
-
- if (!team) {
- redirect("/team");
- }
-
- if (!project) {
- redirect(`/team/${params.team_slug}`);
- }
-
- const interval = searchParams.interval ?? "week";
- const rangeType = searchParams.range || "last-120";
-
- const range: Range = {
- from:
- rangeType === "custom"
- ? searchParams.from
- : getLastNDaysRange(rangeType).from,
- to:
- rangeType === "custom"
- ? searchParams.to
- : getLastNDaysRange(rangeType).to,
- type: rangeType,
- };
-
- const userOpStats = await getUserOpUsage(
- {
- from: range.from,
- period: interval,
- projectId: project.id,
- teamId: project.teamId,
- to: range.to,
- },
- authToken,
- );
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: project.teamId,
- });
-
- const isBundlerServiceEnabled = !!project.services.find(
- (s) => s.name === "bundler",
- );
-
- const hasSmartWalletsWithoutBilling =
- isBundlerServiceEnabled &&
- team.billingStatus !== "validPayment" &&
- team.billingStatus !== "pastDue";
-
- return (
-
- {hasSmartWalletsWithoutBilling && (
- <>
-
-
- >
- )}
-
-
+ const params = await props.params;
+ redirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas/overview`,
);
}
-
-const seo = {
- desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
- title: "The Complete Account Abstraction Toolkit | thirdweb",
-};
-
-export const metadata: Metadata = {
- description: seo.desc,
- openGraph: {
- description: seo.desc,
- images: [
- {
- alt: seo.title,
- height: 630,
- url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
- width: 1200,
- },
- ],
- title: seo.title,
- },
- title: seo.title,
-};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
index aff37b655fe..e702a9a0aff 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
@@ -1,23 +1,22 @@
"use client";
import {
- ArrowLeftRightIcon,
BookTextIcon,
BoxIcon,
DatabaseIcon,
HomeIcon,
- LockIcon,
RssIcon,
Settings2Icon,
WebhookIcon,
} from "lucide-react";
-import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
-import { Badge } from "@/components/ui/badge";
+import {
+ FullWidthSidebarLayout,
+ type ShadcnSidebarLink,
+} from "@/components/blocks/full-width-sidebar-layout";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { PayIcon } from "@/icons/PayIcon";
-import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
import { TokenIcon } from "@/icons/TokenIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
@@ -25,136 +24,136 @@ export function ProjectSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
}) {
- return (
-
- Transactions New
-
- ),
- },
- {
- href: `${props.layoutPath}/contracts`,
- icon: ContractIcon,
- label: "Contracts",
- },
- {
- href: `${props.layoutPath}/ai`,
- icon: NebulaIcon,
- label: "AI",
- },
- ],
- },
+ const contentSidebarLinks = [
+ {
+ exactMatch: true,
+ href: props.layoutPath,
+ icon: HomeIcon,
+ label: "Overview",
+ },
+ {
+ separator: true,
+ },
+ {
+ group: "Build",
+ links: [
{
- separator: true,
- },
- {
- group: "Monetize",
+ subMenu: {
+ icon: WalletProductIcon,
+ label: "Wallets",
+ },
links: [
{
- href: `${props.layoutPath}/payments`,
- icon: PayIcon,
- label: "Payments",
+ href: `${props.layoutPath}/wallets/user-wallets`,
+ label: "User Wallets",
},
{
- href: `${props.layoutPath}/bridge`,
- icon: BridgeIcon,
- label: "Bridge",
+ href: `${props.layoutPath}/wallets/server-wallets`,
+ label: "Server Wallets",
},
{
- href: `${props.layoutPath}/tokens`,
- icon: TokenIcon,
- label: "Tokens",
+ href: `${props.layoutPath}/wallets/sponsored-gas`,
+ label: "Sponsored Gas",
},
],
},
{
- separator: true,
+ href: `${props.layoutPath}/contracts`,
+ icon: ContractIcon,
+ label: "Contracts",
},
{
- 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",
- },
- // linkely want to move this to `team` level eventually
- {
- href: `${props.layoutPath}/engine`,
- icon: DatabaseIcon,
- label: "Engine",
- },
- ],
+ href: `${props.layoutPath}/ai`,
+ icon: NebulaIcon,
+ label: "AI",
},
- ]}
- footerSidebarLinks={[
+ ],
+ },
+ {
+ separator: true,
+ },
+ {
+ group: "Monetize",
+ links: [
{
- separator: true,
+ href: `${props.layoutPath}/payments`,
+ icon: PayIcon,
+ label: "Payments",
},
{
- href: `${props.layoutPath}/webhooks/contracts`,
- icon: WebhookIcon,
- isActive: (pathname) => {
- return pathname.startsWith(`${props.layoutPath}/webhooks`);
- },
- label: "Webhooks",
+ href: `${props.layoutPath}/bridge`,
+ icon: BridgeIcon,
+ label: "Bridge",
},
{
- href: `${props.layoutPath}/settings`,
- icon: Settings2Icon,
- label: "Project Settings",
+ href: `${props.layoutPath}/tokens`,
+ icon: TokenIcon,
+ label: "Tokens",
},
+ ],
+ },
+ {
+ separator: true,
+ },
+ {
+ group: "Scale",
+ links: [
{
- separator: true,
+ href: `${props.layoutPath}/insight`,
+ icon: InsightIcon,
+ label: "Insight",
},
{
- href: "https://portal.thirdweb.com",
- icon: BookTextIcon,
- label: "Documentation",
+ href: `${props.layoutPath}/rpc`,
+ icon: RssIcon,
+ label: "RPC",
},
+ // linkely want to move this to `team` level eventually
{
- href: "https://playground.thirdweb.com/connect/sign-in/button",
- icon: BoxIcon,
- label: "Playground",
+ href: `${props.layoutPath}/engine`,
+ icon: DatabaseIcon,
+ label: "Engine",
},
- ]}
+ ],
+ },
+ ] satisfies ShadcnSidebarLink[];
+
+ const footerSidebarLinks = [
+ {
+ separator: true,
+ },
+ {
+ href: `${props.layoutPath}/webhooks/contracts`,
+ icon: WebhookIcon,
+ isActive: (pathname) => {
+ return pathname.startsWith(`${props.layoutPath}/webhooks`);
+ },
+ label: "Webhooks",
+ },
+ {
+ href: `${props.layoutPath}/settings`,
+ icon: Settings2Icon,
+ label: "Project Settings",
+ },
+ {
+ separator: true,
+ },
+ {
+ href: "https://portal.thirdweb.com",
+ icon: BookTextIcon,
+ label: "Documentation",
+ },
+ {
+ href: "https://playground.thirdweb.com/connect/sign-in/button",
+ icon: BoxIcon,
+ label: "Playground",
+ },
+ ] satisfies ShadcnSidebarLink[];
+
+ return (
+
{props.children}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx
index 8764b31bfed..76ee5ab25e9 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx
@@ -1,95 +1,11 @@
-import { CircleAlertIcon } from "lucide-react";
import { redirect } from "next/navigation";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import { getTeamBySlug } from "@/api/team/get-team";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { UnderlineLink } from "@/components/ui/UnderlineLink";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
-import { loginRedirect } from "@/utils/redirects";
-import { DefaultFactoriesSection } from "../../account-abstraction/factories/AccountFactories";
-import { YourFactoriesSection } from "../../account-abstraction/factories/AccountFactories/your-factories";
-import { AccountAbstractionSettingsPage } from "../../account-abstraction/settings/SponsorshipPolicies";
-import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
+// Redirect old settings/account-abstraction page to new Sponsored Gas Configuration
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const { team_slug, project_slug } = await props.params;
-
- const [team, project, authToken] = await Promise.all([
- getTeamBySlug(team_slug),
- getProject(team_slug, project_slug),
-
- getAuthToken(),
- ]);
-
- if (!authToken) {
- loginRedirect(`/team/${team_slug}/settings/account-abstraction`);
- }
-
- if (!team) {
- redirect("/team");
- }
-
- if (!project) {
- redirect(`/team/${team_slug}`);
- }
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: team.id,
- });
-
- const bundlerService = project.services.find((s) => s.name === "bundler");
-
- return (
-
-
-
-
- {!bundlerService ? (
-
-
- Account Abstraction service is disabled
-
- Enable Account Abstraction service in{" "}
-
- project settings
- {" "}
- to configure the sponsorship rules
-
-
- ) : (
- <>
-
-
-
-
- >
- )}
-
-
+ redirect(
+ `/team/${team_slug}/${project_slug}/wallets/sponsored-gas/configuration`,
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx
index 38319af7c89..ee4f2ee8036 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/page.tsx
@@ -1,59 +1,11 @@
import { redirect } from "next/navigation";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import { getTeamBySlug } from "@/api/team/get-team";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
-import { loginRedirect } from "@/utils/redirects";
-import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
-import { getSMSCountryTiers } from "./api/sms";
-import { InAppWalletSettingsPage } from "./components";
+// Redirect old settings/wallets page to new User Wallets Configuration
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const { team_slug, project_slug } = await props.params;
-
- const [team, project, smsCountryTiers, authToken] = await Promise.all([
- getTeamBySlug(team_slug),
- getProject(team_slug, project_slug),
- getSMSCountryTiers(),
- getAuthToken(),
- ]);
-
- if (!authToken) {
- loginRedirect(`/team/${team_slug}/settings/wallets`);
- }
-
- if (!team) {
- redirect("/team");
- }
-
- if (!project) {
- redirect(`/team/${team_slug}`);
- }
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: team.id,
- });
-
- return (
-
+ redirect(
+ `/team/${team_slug}/${project_slug}/wallets/user-wallets/configuration`,
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
index e9a37e0525b..ef2ce8670e2 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx
@@ -1,224 +1,11 @@
-import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
-import { ArrowLeftRightIcon } from "lucide-react";
-import { notFound, redirect } from "next/navigation";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import { ProjectPage } from "@/components/blocks/project-page/project-page";
-import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page";
-import { EngineChecklist } from "./analytics/ftux.client";
-import { TransactionAnalyticsSummary } from "./analytics/summary";
-import { ServerWalletsTable } from "./components/server-wallets-table.client";
-import { getTransactionAnalyticsSummary } from "./lib/analytics";
-import type { Wallet } from "./server-wallets/wallet-table/types";
-import { listSolanaAccounts } from "./solana-wallets/lib/vault.client";
-import type { SolanaWallet } from "./solana-wallets/wallet-table/types";
+import { redirect } from "next/navigation";
-export const dynamic = "force-dynamic";
-
-export default async function TransactionsAnalyticsPage(props: {
+// Redirect old Transactions page to new Server Wallets Overview
+export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
- searchParams: Promise<{
- from?: string | string[] | undefined;
- to?: string | string[] | undefined;
- interval?: string | string[] | undefined;
- testTxWithWallet?: string | string[] | undefined;
- testSolanaTxWithWallet?: string | string[] | undefined;
- page?: string;
- solana_page?: string;
- }>;
}) {
- const [params, searchParams, authToken] = await Promise.all([
- props.params,
- props.searchParams,
- getAuthToken(),
- ]);
-
- if (!authToken) {
- notFound();
- }
-
- const [vaultClient, project] = await Promise.all([
- createVaultClient({
- baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
- }).catch(() => undefined),
- getProject(params.team_slug, params.project_slug),
- ]);
-
- if (!project) {
- redirect(`/team/${params.team_slug}`);
- }
-
- if (!vaultClient) {
- return Error: Failed to connect to Vault
;
- }
-
- const projectEngineCloudService = project.services.find(
- (service) => service.name === "engineCloud",
- );
-
- const managementAccessToken =
- projectEngineCloudService?.managementAccessToken;
- const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey;
-
- const pageSize = 10;
- const currentPage = Number.parseInt(searchParams.page ?? "1");
- const solanCurrentPage = Number.parseInt(searchParams.solana_page ?? "1");
-
- const eoas = managementAccessToken
- ? await listEoas({
- client: vaultClient,
- request: {
- auth: {
- accessToken: managementAccessToken,
- },
- options: {
- page: currentPage - 1,
- // @ts-expect-error - TODO: fix this
- page_size: pageSize,
- },
- },
- })
- : { data: { items: [], totalRecords: 0 }, error: null, success: true };
-
- const wallets = eoas.data?.items as Wallet[] | undefined;
-
- // Fetch Solana accounts - gracefully handle permission errors
- let solanaAccounts: {
- data: { items: SolanaWallet[]; totalRecords: number };
- error: Error | null;
- success: boolean;
- };
-
- if (managementAccessToken) {
- solanaAccounts = await listSolanaAccounts({
- managementAccessToken,
- page: solanCurrentPage,
- limit: pageSize,
- projectId: project.id,
- });
- } else {
- solanaAccounts = {
- data: { items: [], totalRecords: 0 },
- error: null,
- success: true,
- };
- }
-
- // Check if error is a permission error
- const isSolanaPermissionError =
- solanaAccounts.error?.message.includes("AUTH_INSUFFICIENT_SCOPE") ?? false;
-
- const initialData = await getTransactionAnalyticsSummary({
- clientId: project.publishableKey,
- teamId: project.teamId,
- }).catch(() => undefined);
- const hasTransactions = initialData ? initialData.totalCount > 0 : false;
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: project.teamId,
- });
-
- return (
-
- Send, monitor, and manage transactions.{" "}
- Send transactions from user or
- server wallets, sponsor gas, monitor transaction status, and more
- >
- ),
- actions: null,
- links: [
- {
- type: "docs",
- href: "https://portal.thirdweb.com/transactions",
- },
- {
- type: "playground",
- href: "https://playground.thirdweb.com/transactions/airdrop-tokens",
- },
- {
- type: "api",
- href: "https://api.thirdweb.com/reference#tag/transactions",
- },
- ],
- }}
- >
-
-
- {hasTransactions &&
- !searchParams.testTxWithWallet &&
- !searchParams.testSolanaTxWithWallet && (
-
- )}
-
- {/* transactions */}
-
-
- {/* Server Wallets (EVM + Solana) */}
- {eoas.error ? (
-
-
- EVM Wallet Error
-
-
- {eoas.error.message}
-
-
- ) : (
-
- )}
-
-
+ const params = await props.params;
+ redirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`,
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/page.tsx
index 6c4455e2e1d..2fced3d5b6e 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/page.tsx
@@ -1,58 +1,11 @@
-import { LockIcon } from "lucide-react";
-import { notFound } from "next/navigation";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import { ProjectPage } from "@/components/blocks/project-page/project-page";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { KeyManagement } from "./components/key-management";
+import { redirect } from "next/navigation";
-export default async function VaultPage(props: {
+// Redirect old Vault page to new Server Wallets Configuration
+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 (!project || !authToken) {
- notFound();
- }
-
- const projectEngineCloudService = project.services.find(
- (service) => service.name === "engineCloud",
- );
-
- const maskedAdminKey = projectEngineCloudService?.maskedAdminKey;
- const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey;
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: project.teamId,
- });
-
- return (
-
-
-
+ const params = await props.params;
+ redirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/configuration`,
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/layout.tsx
new file mode 100644
index 00000000000..466b1a5a1f3
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/layout.tsx
@@ -0,0 +1,62 @@
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import { ProjectPage } from "@/components/blocks/project-page/project-page";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { WalletProductIcon } from "@/icons/WalletProductIcon";
+import { loginRedirect } from "@/utils/redirects";
+
+export default async function WalletsLayout(props: {
+ children: React.ReactNode;
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const params = await props.params;
+ const [authToken, project] = await Promise.all([
+ getAuthToken(),
+ getProject(params.team_slug, params.project_slug),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/user-wallets`,
+ );
+ }
+
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
+ }
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: project.teamId,
+ });
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
index 0c080f97206..8767a96f461 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
@@ -1,226 +1,10 @@
-import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { redirect } from "next/navigation";
-import { ResponsiveSearchParamsProvider } from "responsive-rsc";
-import { getAuthToken } from "@/api/auth-token";
-import { getProject } from "@/api/project/projects";
-import type { DurationId } from "@/components/analytics/date-range-selector";
-import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
-import { ProjectPage } from "@/components/blocks/project-page/project-page";
-import { InAppWalletUsersPageContent } from "@/components/in-app-wallet-users-content/in-app-wallet-users-content";
-import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
-import { WalletProductIcon } from "@/icons/WalletProductIcon";
-import { getFiltersFromSearchParams } from "@/lib/time";
-import { loginRedirect } from "@/utils/redirects";
-import { ServerWalletsTable } from "../transactions/components/server-wallets-table.client";
-import type { Wallet } from "../transactions/server-wallets/wallet-table/types";
-import { listSolanaAccounts } from "../transactions/solana-wallets/lib/vault.client";
-import type { SolanaWallet } from "../transactions/solana-wallets/wallet-table/types";
-import { InAppWalletAnalytics } from "./analytics/chart";
-import { InAppWalletsSummary } from "./analytics/chart/Summary";
-
-export const dynamic = "force-dynamic";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
- searchParams: Promise<{
- from?: string;
- to?: string;
- type?: string;
- interval?: string;
- page?: string;
- solana_page?: string;
- }>;
}) {
- const [searchParams, params] = await Promise.all([
- props.searchParams,
- props.params,
- ]);
-
- const authToken = await getAuthToken();
- if (!authToken) {
- loginRedirect(`/team/${params.team_slug}/${params.project_slug}/wallets`);
- }
-
- const defaultRange: DurationId = "last-30";
- const { range, interval } = getFiltersFromSearchParams({
- defaultRange,
- from: searchParams.from,
- interval: searchParams.interval,
- to: searchParams.to,
- });
-
- const [vaultClient, project] = await Promise.all([
- createVaultClient({
- baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
- }).catch(() => undefined),
- getProject(params.team_slug, params.project_slug),
- ]);
-
- if (!project) {
- redirect(`/team/${params.team_slug}`);
- }
-
- const projectEngineCloudService = project.services.find(
- (service) => service.name === "engineCloud",
- );
-
- const managementAccessToken =
- projectEngineCloudService?.managementAccessToken;
-
- // Fetch server wallets with pagination (5 per page)
- const pageSize = 5;
- const currentPage = Number.parseInt(searchParams.page ?? "1");
- const solanCurrentPage = Number.parseInt(searchParams.solana_page ?? "1");
-
- // Fetch EVM wallets
- const eoas =
- vaultClient && managementAccessToken
- ? await listEoas({
- client: vaultClient,
- request: {
- auth: {
- accessToken: managementAccessToken,
- },
- options: {
- page: currentPage - 1,
- // @ts-expect-error - TODO: fix this
- page_size: pageSize,
- },
- },
- })
- : { data: { items: [], totalRecords: 0 }, error: null, success: true };
-
- // Fetch Solana wallets
- let solanaAccounts: {
- data: { items: SolanaWallet[]; totalRecords: number };
- error: Error | null;
- success: boolean;
- };
-
- if (managementAccessToken) {
- solanaAccounts = await listSolanaAccounts({
- managementAccessToken,
- page: solanCurrentPage,
- limit: pageSize,
- projectId: project.id,
- });
- } else {
- solanaAccounts = {
- data: { items: [], totalRecords: 0 },
- error: null,
- success: true,
- };
- }
-
- // Check for Solana permission errors
- const isSolanaPermissionError = solanaAccounts.error?.message?.includes(
- "AUTH_INSUFFICIENT_SCOPE",
- );
-
- const client = getClientThirdwebClient({
- jwt: authToken,
- teamId: project.teamId,
- });
-
- return (
-
-
- Create wallets for your users with flexible authentication
- options.
- Choose from email/phone
- verification, OAuth, passkeys, or external wallet connections
- >
- ),
- actions: null,
- settings: {
- href: `/team/${params.team_slug}/${params.project_slug}/settings/wallets`,
- },
- links: [
- {
- type: "docs",
- href: "https://portal.thirdweb.com/wallets",
- },
- {
- type: "playground",
- href: "https://playground.thirdweb.com/wallets/in-app-wallet",
- },
- {
- type: "api",
- href: "https://api.thirdweb.com/reference#tag/wallets",
- },
- ],
- }}
- >
-
-
-
-
-
-
- {/* Server Wallets Section (EVM + Solana) */}
-
- {eoas.error ? (
-
-
- EVM Wallet Error
-
-
- {eoas.error.message || "Failed to load EVM wallets"}
-
-
- ) : (
-
- )}
-
-
- {/* User Wallets Section */}
-
-
- User wallets
-
-
-
-
-
-
+ const params = await props.params;
+ redirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/user-wallets/overview`,
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/configuration/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/configuration/page.tsx
new file mode 100644
index 00000000000..89916b46b21
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/configuration/page.tsx
@@ -0,0 +1,40 @@
+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 (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/layout.tsx
new file mode 100644
index 00000000000..c414ef4f81c
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/layout.tsx
@@ -0,0 +1,27 @@
+import { TabPathLinks } from "@/components/ui/tabs";
+
+export default async function Layout(props: {
+ children: React.ReactNode;
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const params = await props.params;
+ const basePath = `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets`;
+
+ return (
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/overview/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/overview/page.tsx
new file mode 100644
index 00000000000..2da3395af79
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/overview/page.tsx
@@ -0,0 +1,185 @@
+import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { TransactionsAnalyticsPageContent } from "../../../transactions/analytics/analytics-page";
+import { EngineChecklist } from "../../../transactions/analytics/ftux.client";
+import { TransactionAnalyticsSummary } from "../../../transactions/analytics/summary";
+import { ServerWalletsTable } from "../../../transactions/components/server-wallets-table.client";
+import { getTransactionAnalyticsSummary } from "../../../transactions/lib/analytics";
+import type { Wallet } from "../../../transactions/server-wallets/wallet-table/types";
+import { listSolanaAccounts } from "../../../transactions/solana-wallets/lib/vault.client";
+import type { SolanaWallet } from "../../../transactions/solana-wallets/wallet-table/types";
+
+export const dynamic = "force-dynamic";
+
+export default async function Page(props: {
+ params: Promise<{ team_slug: string; project_slug: string }>;
+ searchParams: Promise<{
+ from?: string | string[];
+ to?: string | string[];
+ interval?: string | string[];
+ testTxWithWallet?: string | string[];
+ testSolanaTxWithWallet?: string | string[];
+ page?: string;
+ solana_page?: string;
+ }>;
+}) {
+ const [params, searchParams, authToken] = await Promise.all([
+ props.params,
+ props.searchParams,
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ redirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/server-wallets/overview`,
+ );
+ }
+
+ const [vaultClient, project] = await Promise.all([
+ createVaultClient({
+ baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
+ }).catch(() => undefined),
+ getProject(params.team_slug, params.project_slug),
+ ]);
+
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
+ }
+
+ const projectEngineCloudService = project.services.find(
+ (service) => service.name === "engineCloud",
+ );
+
+ const managementAccessToken =
+ projectEngineCloudService?.managementAccessToken;
+ const isManagedVault = !!projectEngineCloudService?.encryptedAdminKey;
+
+ const pageSize = 10;
+ const currentPage = Number.parseInt(searchParams.page ?? "1");
+ const solanaCurrentPage = Number.parseInt(searchParams.solana_page ?? "1");
+
+ const eoas =
+ managementAccessToken && vaultClient
+ ? await listEoas({
+ client: vaultClient,
+ request: {
+ auth: {
+ accessToken: managementAccessToken,
+ },
+ options: {
+ page: currentPage - 1,
+ // @ts-expect-error - TODO: fix this
+ page_size: pageSize,
+ },
+ },
+ })
+ : { data: { items: [], totalRecords: 0 }, error: null, success: true };
+
+ const wallets = eoas.data?.items as Wallet[] | undefined;
+
+ let solanaAccounts: {
+ data: { items: SolanaWallet[]; totalRecords: number };
+ error: Error | null;
+ success: boolean;
+ };
+
+ if (managementAccessToken) {
+ solanaAccounts = await listSolanaAccounts({
+ managementAccessToken,
+ page: solanaCurrentPage,
+ limit: pageSize,
+ projectId: project.id,
+ });
+ } else {
+ solanaAccounts = {
+ data: { items: [], totalRecords: 0 },
+ error: null,
+ success: true,
+ };
+ }
+
+ const isSolanaPermissionError =
+ solanaAccounts.error?.message.includes("AUTH_INSUFFICIENT_SCOPE") ?? false;
+
+ const initialData = await getTransactionAnalyticsSummary({
+ clientId: project.publishableKey,
+ teamId: project.teamId,
+ }).catch(() => undefined);
+ const hasTransactions = initialData ? initialData.totalCount > 0 : false;
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: project.teamId,
+ });
+
+ return (
+
+
+ {hasTransactions &&
+ !searchParams.testTxWithWallet &&
+ !searchParams.testSolanaTxWithWallet && (
+
+ )}
+
+
+
+ {eoas.error ? (
+
+
+ EVM Wallet Error
+
+
{eoas.error.message}
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/page.tsx
new file mode 100644
index 00000000000..fd0c6686f24
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/page.tsx
@@ -0,0 +1,10 @@
+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/server-wallets/overview`,
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/configuration/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/configuration/page.tsx
new file mode 100644
index 00000000000..1ab1ab0a8b1
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/configuration/page.tsx
@@ -0,0 +1,85 @@
+import { CircleAlertIcon } from "lucide-react";
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import { getTeamBySlug } from "@/api/team/get-team";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
+import { loginRedirect } from "@/utils/redirects";
+import { DefaultFactoriesSection } from "../../../account-abstraction/factories/AccountFactories";
+import { YourFactoriesSection } from "../../../account-abstraction/factories/AccountFactories/your-factories";
+import { AccountAbstractionSettingsPage } from "../../../account-abstraction/settings/SponsorshipPolicies";
+
+export default async function Page(props: {
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const { team_slug, project_slug } = await props.params;
+
+ const [team, project, authToken] = await Promise.all([
+ getTeamBySlug(team_slug),
+ getProject(team_slug, project_slug),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(
+ `/team/${team_slug}/${project_slug}/wallets/sponsored-gas/configuration`,
+ );
+ }
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ if (!project) {
+ redirect(`/team/${team_slug}`);
+ }
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: team.id,
+ });
+
+ const bundlerService = project.services.find((s) => s.name === "bundler");
+
+ return (
+
+ {!bundlerService ? (
+
+
+ Account Abstraction service is disabled
+
+ Enable Account Abstraction service in{" "}
+
+ project settings
+ {" "}
+ to configure the sponsorship rules
+
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/layout.tsx
new file mode 100644
index 00000000000..8f3f5f6fc19
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/layout.tsx
@@ -0,0 +1,27 @@
+import { TabPathLinks } from "@/components/ui/tabs";
+
+export default async function Layout(props: {
+ children: React.ReactNode;
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const params = await props.params;
+ const basePath = `/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas`;
+
+ return (
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/overview/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/overview/page.tsx
new file mode 100644
index 00000000000..4abeb963a70
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/overview/page.tsx
@@ -0,0 +1,111 @@
+import { notFound, redirect } from "next/navigation";
+import type { SearchParams } from "nuqs/server";
+import { getUserOpUsage } from "@/api/analytics";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import { getTeamBySlug } from "@/api/team/get-team";
+import {
+ getLastNDaysRange,
+ type Range,
+} from "@/components/analytics/date-range-selector";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { AccountAbstractionSummary } from "../../../account-abstraction/AccountAbstractionAnalytics/AccountAbstractionSummary";
+import { SmartWalletsBillingAlert } from "../../../account-abstraction/Alerts";
+import { AccountAbstractionAnalytics } from "../../../account-abstraction/aa-analytics";
+import { searchParamLoader } from "../../../account-abstraction/search-params";
+
+export const dynamic = "force-dynamic";
+
+export default async function Page(props: {
+ params: Promise<{ team_slug: string; project_slug: string }>;
+ searchParams: Promise;
+}) {
+ const [params, searchParams, authToken] = await Promise.all([
+ props.params,
+ searchParamLoader(props.searchParams),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ notFound();
+ }
+
+ const [team, project] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getProject(params.team_slug, params.project_slug),
+ ]);
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
+ }
+
+ const interval = searchParams.interval ?? "week";
+ const rangeType = searchParams.range || "last-120";
+
+ const range: Range = {
+ from:
+ rangeType === "custom"
+ ? searchParams.from
+ : getLastNDaysRange(rangeType).from,
+ to:
+ rangeType === "custom"
+ ? searchParams.to
+ : getLastNDaysRange(rangeType).to,
+ type: rangeType,
+ };
+
+ const userOpStats = await getUserOpUsage(
+ {
+ from: range.from,
+ period: interval,
+ projectId: project.id,
+ teamId: project.teamId,
+ to: range.to,
+ },
+ authToken,
+ );
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: project.teamId,
+ });
+
+ const isBundlerServiceEnabled = !!project.services.find(
+ (s) => s.name === "bundler",
+ );
+
+ const hasSmartWalletsWithoutBilling =
+ isBundlerServiceEnabled &&
+ team.billingStatus !== "validPayment" &&
+ team.billingStatus !== "pastDue";
+
+ return (
+ <>
+ {hasSmartWalletsWithoutBilling && (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/page.tsx
new file mode 100644
index 00000000000..8de1824fbf6
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/sponsored-gas/page.tsx
@@ -0,0 +1,10 @@
+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/sponsored-gas/overview`,
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/api/sms.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/api/sms.ts
new file mode 100644
index 00000000000..0ddb49eb77f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/api/sms.ts
@@ -0,0 +1,61 @@
+import "server-only";
+import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
+import { API_SERVER_SECRET } from "@/constants/server-envs";
+
+export type SMSCountryTiers = {
+ tier1: string[];
+ tier2: string[];
+ tier3: string[];
+ tier4: string[];
+ tier5: string[];
+ tier6: string[];
+};
+
+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: [],
+ };
+ }
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/index.tsx
new file mode 100644
index 00000000000..af25993589c
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/index.tsx
@@ -0,0 +1,891 @@
+"use client";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation } from "@tanstack/react-query";
+import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils";
+import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
+import type React from "react";
+import { useState } from "react";
+import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
+import { toast } from "sonner";
+import type { ThirdwebClient } from "thirdweb";
+import { upload } from "thirdweb/storage";
+import type { Project } from "@/api/project/projects";
+import type { Team } from "@/api/team/get-team";
+import { FileInput } from "@/components/blocks/FileInput";
+import { GatedSwitch } from "@/components/blocks/GatedSwitch";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { DynamicHeight } from "@/components/ui/DynamicHeight";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Spinner } from "@/components/ui/Spinner";
+import { Textarea } from "@/components/ui/textarea";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+import { planToTierRecordForGating } from "@/constants/planToTierRecord";
+import { updateProjectClient } from "@/hooks/useApi";
+import { cn } from "@/lib/utils";
+import {
+ type ApiKeyEmbeddedWalletsValidationSchema,
+ apiKeyEmbeddedWalletsValidationSchema,
+} from "@/schema/validations";
+import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
+import { toArrFromList } from "@/utils/string";
+import type { SMSCountryTiers } from "../api/sms";
+import { CountrySelector } from "./sms-country-select";
+
+type InAppWalletSettingsPageProps = {
+ project: Project;
+ teamId: string;
+ teamSlug: string;
+ teamPlan: Team["billingPlan"];
+ smsCountryTiers: SMSCountryTiers;
+ client: ThirdwebClient;
+};
+
+type UpdateAPIKeyTrackingData = {
+ hasCustomBranding: boolean;
+ hasCustomJwt: boolean;
+ hasCustomAuthEndpoint: boolean;
+};
+
+export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
+ const updateProject = useMutation({
+ mutationFn: async (projectValues: Partial) => {
+ await updateProjectClient(
+ {
+ projectId: props.project.id,
+ teamId: props.teamId,
+ },
+ projectValues,
+ );
+ },
+ });
+
+ function handleUpdateProject(projectValues: Partial) {
+ updateProject.mutate(projectValues, {
+ onError: (err) => {
+ toast.error("Failed to update an API Key");
+ console.error(err);
+ },
+ onSuccess: () => {
+ toast.success("In-App Wallet API Key configuration updated");
+ },
+ });
+ }
+
+ return (
+
+ );
+}
+
+const InAppWalletSettingsPageUI: React.FC<
+ InAppWalletSettingsPageProps & {
+ updateApiKey: (
+ projectValues: Partial,
+ trackingData: UpdateAPIKeyTrackingData,
+ ) => void;
+ isUpdating: boolean;
+ smsCountryTiers: SMSCountryTiers;
+ }
+> = (props) => {
+ const embeddedWalletService = props.project.services.find(
+ (service) => service.name === "embeddedWallets",
+ );
+
+ if (!embeddedWalletService) {
+ return (
+
+
+ In-App wallets service is disabled
+
+ Enable In-App wallets service in the{" "}
+
+ project settings
+ {" "}
+ to configure settings
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export const InAppWalletSettingsUI: React.FC<
+ InAppWalletSettingsPageProps & {
+ updateApiKey: (
+ projectValues: Partial,
+ trackingData: UpdateAPIKeyTrackingData,
+ ) => void;
+ isUpdating: boolean;
+ embeddedWalletService: ProjectEmbeddedWalletsService;
+ client: ThirdwebClient;
+ }
+> = (props) => {
+ const services = props.project.services;
+
+ const config = props.embeddedWalletService;
+
+ const hasCustomBranding =
+ !!config.applicationImageUrl?.length || !!config.applicationName?.length;
+
+ const authRequiredPlan = "growth";
+ const brandingRequiredPlan = "starter";
+
+ // growth or higher plan required
+ const canEditSmsCountries =
+ planToTierRecordForGating[props.teamPlan] >=
+ planToTierRecordForGating[authRequiredPlan];
+
+ const form = useForm({
+ resolver: zodResolver(apiKeyEmbeddedWalletsValidationSchema),
+ values: {
+ customAuthEndpoint: config.customAuthEndpoint || undefined,
+ customAuthentication: config.customAuthentication || undefined,
+ ...(hasCustomBranding
+ ? {
+ branding: {
+ applicationImageUrl: config.applicationImageUrl || undefined,
+ applicationName: config.applicationName || undefined,
+ },
+ }
+ : undefined),
+ redirectUrls: (config.redirectUrls || []).join("\n"),
+ smsEnabledCountryISOs: config.smsEnabledCountryISOs
+ ? config.smsEnabledCountryISOs
+ : canEditSmsCountries
+ ? ["US", "CA"]
+ : [],
+ },
+ });
+
+ const handleSubmit = form.handleSubmit((values) => {
+ const { customAuthentication, customAuthEndpoint, branding, redirectUrls } =
+ values;
+
+ if (
+ customAuthentication &&
+ (!customAuthentication.aud.length || !customAuthentication.jwksUri.length)
+ ) {
+ return toast.error("Custom JSON Web Token configuration is invalid", {
+ description:
+ "To use In-App Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
+ dismissible: true,
+ duration: 9000,
+ });
+ }
+
+ if (customAuthEndpoint && !customAuthEndpoint.authEndpoint.length) {
+ return toast.error(
+ "Custom Authentication Endpoint configuration is invalid",
+ {
+ description:
+ "To use In-App Wallets with Custom Authentication Endpoint, provide a valid URL.",
+ dismissible: true,
+ duration: 9000,
+ },
+ );
+ }
+
+ const newServices = services.map((service) => {
+ if (service.name !== "embeddedWallets") {
+ return service;
+ }
+
+ return {
+ ...service,
+ applicationImageUrl: branding?.applicationImageUrl,
+ applicationName: branding?.applicationName || props.project.name,
+ customAuthEndpoint,
+ customAuthentication,
+ redirectUrls: toArrFromList(redirectUrls || "", true),
+ smsEnabledCountryISOs: values.smsEnabledCountryISOs,
+ };
+ });
+
+ props.updateApiKey(
+ {
+ services: newServices,
+ },
+ {
+ hasCustomAuthEndpoint: !!customAuthEndpoint,
+ hasCustomBranding: !!branding,
+ hasCustomJwt: !!customAuthentication,
+ },
+ );
+ });
+
+ return (
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+function BrandingFieldset(props: {
+ form: UseFormReturn;
+ teamPlan: Team["billingPlan"];
+ teamSlug: string;
+ requiredPlan: Team["billingPlan"];
+ client: ThirdwebClient;
+ isUpdating: boolean;
+}) {
+ return (
+
+
+ {props.isUpdating && }
+ Save
+
+
+ }
+ >
+
+
+ props.form.setValue(
+ "branding",
+ checked
+ ? {
+ applicationImageUrl: "",
+ applicationName: "",
+ }
+ : undefined,
+ ),
+ }}
+ teamSlug={props.teamSlug}
+ trackingLabel="customEmailLogoAndName"
+ />
+
+
+
+
+
+
+ );
+}
+
+function AppImageFormControl(props: {
+ uri: string | undefined;
+ setUri: (uri: string) => void;
+ client: ThirdwebClient;
+}) {
+ const [image, setImage] = useState();
+ const resolveUrl = resolveSchemeWithErrorHandler({
+ client: props.client,
+ uri: props.uri || undefined,
+ });
+
+ const uploadImage = useMutation({
+ mutationFn: async (file: File) => {
+ const uri = await upload({
+ client: props.client,
+ files: [file],
+ });
+
+ return uri;
+ },
+ });
+
+ return (
+
+
+
{
+ try {
+ setImage(v);
+ const uri = await uploadImage.mutateAsync(v);
+ props.setUri(uri);
+ } catch (error) {
+ setImage(undefined);
+ toast.error("Failed to upload image", {
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ }}
+ value={image || resolveUrl}
+ />
+
+ {uploadImage.isPending && (
+
+
+
+ )}
+
+
+ );
+}
+
+function SMSCountryFields(props: {
+ form: UseFormReturn;
+ smsCountryTiers: SMSCountryTiers;
+ teamPlan: Team["billingPlan"];
+ requiredPlan: Team["billingPlan"];
+ teamSlug: string;
+}) {
+ return (
+
+
+
+ props.form.setValue(
+ "smsEnabledCountryISOs",
+ checked ? ["US", "CA"] : [],
+ ),
+ }}
+ teamSlug={props.teamSlug}
+ trackingLabel="sms"
+ />
+
+
+
+ (
+
+ )}
+ />
+
+
+ );
+}
+
+function JSONWebTokenFields(props: {
+ form: UseFormReturn;
+ teamPlan: Team["billingPlan"];
+ teamSlug: string;
+ requiredPlan: Team["billingPlan"];
+}) {
+ return (
+
+
+ Optionally allow users to authenticate with a custom JWT.{" "}
+
+ Learn more
+
+ >
+ }
+ switchId="authentication-switch"
+ title="Custom JSON Web Token"
+ >
+ {
+ props.form.setValue(
+ "customAuthentication",
+ checked ? { aud: "", jwksUri: "" } : undefined,
+ );
+ },
+ }}
+ teamSlug={props.teamSlug}
+ trackingLabel="customAuthJWT"
+ />
+
+
+
+ (
+
+ JWKS URI
+
+
+
+ Enter the URI of the JWKS
+
+
+ )}
+ />
+
+ (
+
+ AUD Value
+
+
+
+
+ Enter the audience claim for the JWT
+
+
+
+ )}
+ />
+
+
+ );
+}
+
+function AuthEndpointFields(props: {
+ form: UseFormReturn;
+ teamPlan: Team["billingPlan"];
+ teamSlug: string;
+ requiredPlan: Team["billingPlan"];
+}) {
+ const expandCustomAuthEndpointField =
+ props.form.watch("customAuthEndpoint") !== undefined;
+
+ return (
+
+
+ Optionally allow users to authenticate with any arbitrary payload
+ that you provide.{" "}
+
+ Learn more
+
+ >
+ }
+ switchId="auth-endpoint-switch"
+ title="Custom Authentication Endpoint"
+ >
+ {
+ props.form.setValue(
+ "customAuthEndpoint",
+ checked
+ ? {
+ authEndpoint: "",
+ customHeaders: [],
+ }
+ : undefined,
+ );
+ },
+ }}
+ teamSlug={props.teamSlug}
+ trackingLabel="customAuthEndpoint"
+ />
+
+
+ {/* useFieldArray used on this component - it creates empty customAuthEndpoint.customHeaders array on mount */}
+ {/* So only mount if expandCustomAuthEndpointField is true */}
+ {expandCustomAuthEndpointField && (
+
+ )}
+
+ );
+}
+
+function AuthEndpointFieldsContent(props: {
+ form: UseFormReturn;
+}) {
+ const customHeaderFields = useFieldArray({
+ control: props.form.control,
+ name: "customAuthEndpoint.customHeaders",
+ });
+
+ return (
+
+
(
+
+ Authentication Endpoint
+
+
+
+
+ Enter the URL of your server where we will send the user payload
+ for verification
+
+
+
+ )}
+ />
+
+
+
Custom Headers
+
+
+
+ Set custom headers to be sent along the request with the payload to
+ the authentication endpoint above. This can be used to verify the
+ incoming requests
+
+
+
+ );
+}
+
+function NativeAppsFieldset(props: {
+ form: UseFormReturn;
+ isUpdating: boolean;
+}) {
+ const { form } = props;
+ return (
+
+
+ {props.isUpdating && }
+ Save
+
+
+ }
+ >
+ (
+
+ Allowed redirect URIs
+
+
+
+
+ Enter redirect URIs separated by commas or new lines. This is
+ often your application's deep link.
+
+ Currently only used in Unity, Unreal Engine and React Native
+ platform when users authenticate through social logins.
+
+
+
+ )}
+ />
+
+ );
+}
+
+function GatedCollapsibleContainer(props: {
+ children: React.ReactNode;
+ isExpanded: boolean;
+ className?: string;
+ requiredPlan: Team["billingPlan"];
+ currentPlan: Team["billingPlan"];
+}) {
+ const upgradeRequired =
+ planToTierRecordForGating[props.currentPlan] <
+ planToTierRecordForGating[props.requiredPlan];
+
+ if (!props.isExpanded || upgradeRequired) {
+ return null;
+ }
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+function Fieldset(props: {
+ legend: string;
+ children: React.ReactNode;
+ footer?: React.ReactNode;
+}) {
+ return (
+
+
+
+ {/* put inside div to remove default styles on legend */}
+
+ {props.legend}
+
+
+ {props.children}
+
+
+ {props.footer}
+
+ );
+}
+
+function FieldsetWithDescription(props: {
+ legend: string;
+ children: React.ReactNode;
+ footer?: React.ReactNode;
+ description: React.ReactNode;
+}) {
+ return (
+
+
+
+ {/* put inside div to remove default styles on legend */}
+
+
+ {props.legend}
+
+
{props.description}
+
+
+ {props.children}
+
+
+ {props.footer}
+
+ );
+}
+
+function SwitchContainer(props: {
+ switchId: string;
+ title: string;
+ description: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {props.title}
+
+
+ {props.description}
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/country-selector.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/country-selector.tsx
new file mode 100644
index 00000000000..cdc6f2863f3
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/country-selector.tsx
@@ -0,0 +1,210 @@
+/** biome-ignore-all lint/a11y/useSemanticElements: EXPECTED */
+
+import { CheckIcon, ChevronDownIcon } from "lucide-react";
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { cn } from "@/lib/utils";
+import type { SMSCountryTiers } from "../../api/sms";
+import { countryNames, countryPrefixes, getCountryFlag, tierPricing } from ".";
+
+interface CountrySelectorProps {
+ countryTiers: SMSCountryTiers;
+ selected: string[];
+ onChange: (selectedCountries: string[]) => void;
+}
+
+export default function CountrySelector({
+ countryTiers,
+ selected,
+ onChange,
+}: CountrySelectorProps) {
+ // Helper function to check if a country is selected
+ const isCountrySelected = (country: string) => selected.includes(country);
+
+ // Check if all countries in a tier are selected
+ const isTierSelected = (tier: string) => {
+ const tierCountries = countryTiers[tier as keyof typeof countryTiers];
+ return tierCountries.every((country) => isCountrySelected(country));
+ };
+
+ // Toggle a tier selection
+ const toggleTier = (tier: string) => {
+ const tierCountries = countryTiers[tier as keyof typeof countryTiers];
+ let newSelected: string[];
+
+ if (isTierSelected(tier)) {
+ // Deselect all countries in this tier
+ newSelected = selected.filter(
+ (country) => !tierCountries.includes(country),
+ );
+ } else {
+ // Select all countries in this tier
+ const currentSelected = new Set(selected);
+ for (const country of tierCountries) {
+ currentSelected.add(country);
+ }
+ newSelected = Array.from(currentSelected);
+ }
+
+ // Call onChange with the updated selection
+ onChange(newSelected);
+ };
+
+ // Toggle a single country selection
+ const toggleCountry = (country: string) => {
+ let newSelected: string[];
+
+ if (isCountrySelected(country)) {
+ // Remove country from selection
+ newSelected = selected.filter((c) => c !== country);
+ } else {
+ // Add country to selection
+ newSelected = [...selected, country];
+ }
+
+ // Call onChange with the updated selection
+ onChange(newSelected);
+ };
+
+ return (
+
+ {Object.entries(countryTiers).map(([tier, tierCountries], index) => {
+ const selectedTierCountries = tierCountries.filter((country) =>
+ isCountrySelected(country),
+ );
+
+ return (
+ toggleTier(tier)}
+ tierCountries={tierCountries}
+ selectedTierCountries={selectedTierCountries}
+ onToggleCountry={toggleCountry}
+ />
+ );
+ })}
+
+ );
+}
+
+function TierCard(props: {
+ tier: string;
+ tierIndex: number;
+ onToggleTier: () => void;
+ tierCountries: string[];
+ selectedTierCountries: string[];
+ onToggleCountry: (country: string) => void;
+}) {
+ const {
+ tier,
+ tierIndex,
+ onToggleTier,
+ tierCountries: countries,
+ selectedTierCountries: selectedCountries,
+ onToggleCountry,
+ } = props;
+
+ const [isExpanded, setIsExpanded] = useState(true);
+ const isPartiallySelected =
+ selectedCountries.length > 0 && selectedCountries.length < countries.length;
+ const isTierFullySelected = selectedCountries.length === countries.length;
+
+ return (
+
+ {/* header */}
+
+ {/* left */}
+
+
+
+ Tier {tierIndex + 1}
+
+
+ {isPartiallySelected && (
+
+ ({selectedCountries.length}/{countries.length})
+
+ )}
+
+
+ {/* right */}
+
+
+ {tierPricing[tier as keyof typeof tierPricing]}
+
+
+ setIsExpanded(!isExpanded)}
+ >
+
+
+
+
+
+ {/* body */}
+ {isExpanded && (
+
+ {countries.map((country) => {
+ const isSelected = selectedCountries.includes(country);
+ return (
+
onToggleCountry(country)}
+ title={countryNames[country] || country}
+ >
+
+
{getCountryFlag(country)}
+
+
+ {countryNames[country] || country}
+
+
+ {countryPrefixes[country]
+ ? `${countryPrefixes[country]}`
+ : ""}
+
+
+
+ {isSelected && }
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/index.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/index.ts
new file mode 100644
index 00000000000..9874e95d678
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/index.ts
@@ -0,0 +1,7 @@
+export { default as CountrySelector } from "./country-selector";
+export {
+ countryNames,
+ countryPrefixes,
+ getCountryFlag,
+ tierPricing,
+} from "./utils";
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/utils.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/utils.ts
new file mode 100644
index 00000000000..8e2541fd84f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/components/sms-country-select/utils.ts
@@ -0,0 +1,482 @@
+// Pricing for each tier
+export const tierPricing = {
+ tier1: "Included",
+ tier2: "$0.05 per SMS",
+ tier3: "$0.10 per SMS",
+ tier4: "$0.20 per SMS",
+ tier5: "$0.40 per SMS",
+ tier6: "$1.00 per SMS",
+} as const;
+
+// Country names mapped to ISO codes
+export const countryNames: Record = {
+ // Tier 4
+ AD: "Andorra",
+ AE: "United Arab Emirates",
+
+ // Tier 5
+ AF: "Afghanistan",
+ AG: "Antigua and Barbuda",
+ AI: "Anguilla",
+ AL: "Albania",
+ AM: "Armenia",
+ AO: "Angola",
+
+ // Tier 6
+ RU: "Russia/Kazakhstan",
+ PG: "Papua New Guinea",
+ UZ: "Uzbekistan",
+ TZ: "Tanzania",
+ KM: "Comoros",
+ BT: "Bhutan",
+
+ // Tier 3
+ AR: "Argentina",
+ AS: "American Samoa",
+ AT: "Austria",
+ AU: "Australia",
+ AW: "Aruba",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BB: "Barbados",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BF: "Burkina Faso",
+ BG: "Bulgaria",
+
+ // Tier 2
+ BH: "Bahrain",
+ BI: "Burundi",
+ BJ: "Benin",
+ BM: "Bermuda",
+ BN: "Brunei",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BS: "Bahamas",
+ BW: "Botswana",
+ BY: "Belarus",
+ BZ: "Belize",
+ // Tier 1
+ CA: "Canada",
+ CD: "DR Congo",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Ivory Coast",
+ CK: "Cook Islands",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CU: "Cuba",
+ CV: "Cape Verde",
+ CY: "Cyprus",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DJ: "Djibouti",
+ DK: "Denmark",
+ DM: "Dominica",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FJ: "Fiji",
+ FK: "Falkland Islands",
+ FM: "Micronesia",
+ FO: "Faroe Islands",
+ FR: "France",
+ GA: "Gabon",
+ GB: "United Kingdom",
+ GD: "Grenada",
+ GE: "Georgia",
+ GF: "French Guiana",
+ GH: "Ghana",
+ GI: "Gibraltar",
+ GL: "Greenland",
+ GM: "Gambia",
+ GN: "Guinea",
+ GP: "Guadeloupe",
+ GQ: "Equatorial Guinea",
+ GR: "Greece",
+ GT: "Guatemala",
+ GU: "Guam",
+ GW: "Guinea-Bissau",
+ GY: "Guyana",
+ HK: "Hong Kong",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KI: "Kiribati",
+ KN: "Saint Kitts and Nevis",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KY: "Cayman Islands",
+ KZ: "Kazakhstan",
+ LA: "Laos PDR",
+ LB: "Lebanon",
+ LC: "Saint Lucia",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LR: "Liberia",
+ LS: "Lesotho",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Monaco",
+ MD: "Moldova",
+ ME: "Montenegro",
+ MG: "Madagascar",
+ MH: "Marshall Islands",
+ MK: "North Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macao",
+ MQ: "Martinique",
+ MR: "Mauritania",
+ MS: "Montserrat",
+ MT: "Malta",
+ MU: "Mauritius",
+ MV: "Maldives",
+ MW: "Malawi",
+ MX: "Mexico",
+ MY: "Malaysia",
+ MZ: "Mozambique",
+ NA: "Namibia",
+ NC: "New Caledonia",
+ NE: "Niger",
+ NF: "Norfolk Island",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NU: "Niue",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PF: "French Polynesia",
+ PH: "Philippines",
+ PK: "Pakistan",
+ PL: "Poland",
+ PM: "Saint Pierre and Miquelon",
+ PR: "Puerto Rico",
+ PS: "Palestinian Territory",
+ PT: "Portugal",
+ PW: "Palau",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Réunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SB: "Solomon Islands",
+ SC: "Seychelles",
+ SD: "Sudan",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovakia",
+ SL: "Sierra Leone",
+ SM: "San Marino",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SS: "South Sudan",
+ ST: "Sao Tome and Principe",
+ SV: "El Salvador",
+ SY: "Syria",
+ SZ: "Eswatini",
+ TC: "Turks and Caicos Islands",
+ TD: "Chad",
+ TG: "Togo",
+ TH: "Thailand",
+ TL: "East Timor",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TO: "Tonga",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TV: "Tuvalu",
+ TW: "Taiwan",
+ UA: "Ukraine",
+ UG: "Uganda",
+ US: "United States",
+ UY: "Uruguay",
+ VC: "Saint Vincent and the Grenadines",
+ VE: "Venezuela",
+ VG: "Virgin Islands, British",
+ VI: "Virgin Islands, U.S.",
+ VN: "Vietnam",
+ VU: "Vanuatu",
+ WF: "Wallis and Futuna",
+ WS: "Samoa",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZM: "Zambia",
+ ZW: "Zimbabwe",
+} as const;
+
+export const countryPrefixes: Record = {
+ // Tier 4
+ AD: "+376",
+ AE: "+971",
+
+ // Tier 5
+ AF: "+93",
+ AG: "+1",
+ AI: "+1",
+ AL: "+355",
+ AM: "+374",
+ AO: "+244",
+
+ // Tier 6
+ RU: "+7",
+ PG: "+675",
+ UZ: "+998",
+ TZ: "+255",
+ KM: "+269",
+ BT: "+975",
+
+ // Tier 3
+ AR: "+54",
+ AS: "+1",
+ AT: "+43",
+ AU: "+61",
+ AW: "+297",
+ AZ: "+994",
+ BA: "+387",
+ BB: "+1",
+ BD: "+880",
+ BE: "+32",
+ BF: "+226",
+ BG: "+359",
+
+ // Tier 2
+ BH: "+973",
+ BI: "+257",
+ BJ: "+229",
+ BM: "+1",
+ BN: "+673",
+ BO: "+591",
+ BR: "+55",
+ BS: "+1",
+ BW: "+267",
+ BY: "+375",
+ BZ: "+501",
+ // Tier 1
+ CA: "+1",
+ CD: "+243",
+ CG: "+242",
+ CH: "+41",
+ CI: "+225",
+ CK: "+682",
+ CL: "+56",
+ CM: "+237",
+ CN: "+86",
+ CO: "+57",
+ CR: "+506",
+ CU: "+53",
+ CV: "+238",
+ CY: "+357",
+ CZ: "+420",
+ DE: "+49",
+ DJ: "+253",
+ DK: "+45",
+ DM: "+1",
+ DO: "+1",
+ DZ: "+213",
+ EC: "+593",
+ EE: "+372",
+ EG: "+20",
+ ER: "+291",
+ ES: "+34",
+ ET: "+251",
+ FI: "+358",
+ FJ: "+679",
+ FK: "+500",
+ FM: "+691",
+ FO: "+298",
+ FR: "+33",
+ GA: "+241",
+ GB: "+44",
+ GD: "+1",
+ GE: "+995",
+ GF: "+594",
+ GH: "+233",
+ GI: "+350",
+ GL: "+299",
+ GM: "+220",
+ GN: "+224",
+ GP: "+590",
+ GQ: "+240",
+ GR: "+30",
+ GT: "+502",
+ GU: "+1",
+ GW: "+245",
+ GY: "+592",
+ HK: "+852",
+ HN: "+504",
+ HR: "+385",
+ HT: "+509",
+ HU: "+36",
+ ID: "+62",
+ IE: "+353",
+ IL: "+972",
+ IN: "+91",
+ IQ: "+964",
+ IS: "+354",
+ IT: "+39",
+ JM: "+1",
+ JO: "+962",
+ JP: "+81",
+ KE: "+254",
+ KG: "+996",
+ KH: "+855",
+ KI: "+686",
+ KN: "+1",
+ KR: "+82",
+ KW: "+965",
+ KY: "+1",
+ KZ: "+7",
+ LA: "+856",
+ LB: "+961",
+ LC: "+1",
+ LI: "+423",
+ LK: "+94",
+ LR: "+231",
+ LS: "+266",
+ LT: "+370",
+ LU: "+352",
+ LV: "+371",
+ LY: "+218",
+ MA: "+212",
+ MC: "+377",
+ MD: "+373",
+ ME: "+382",
+ MG: "+261",
+ MH: "+692",
+ MK: "+389",
+ ML: "+223",
+ MM: "+95",
+ MN: "+976",
+ MO: "+853",
+ MQ: "+596",
+ MR: "+222",
+ MS: "+1",
+ MT: "+356",
+ MU: "+230",
+ MV: "+960",
+ MW: "+265",
+ MX: "+52",
+ MY: "+60",
+ MZ: "+258",
+ NA: "+264",
+ NC: "+687",
+ NE: "+227",
+ NF: "+672",
+ NG: "+234",
+ NI: "+505",
+ NL: "+31",
+ NO: "+47",
+ NP: "+977",
+ NU: "+683",
+ NZ: "+64",
+ OM: "+968",
+ PA: "+507",
+ PE: "+51",
+ PF: "+689",
+ PH: "+63",
+ PK: "+92",
+ PL: "+48",
+ PM: "+508",
+ PR: "+1",
+ PS: "+970",
+ PT: "+351",
+ PW: "+680",
+ PY: "+595",
+ QA: "+974",
+ RE: "+262",
+ RO: "+40",
+ RS: "+381",
+ RW: "+250",
+ SA: "+966",
+ SB: "+677",
+ SC: "+248",
+ SD: "+249",
+ SE: "+46",
+ SG: "+65",
+ SI: "+386",
+ SK: "+421",
+ SL: "+232",
+ SM: "+378",
+ SN: "+221",
+ SO: "+252",
+ SR: "+597",
+ SS: "+211",
+ ST: "+239",
+ SV: "+503",
+ SY: "+963",
+ SZ: "+268",
+ TC: "+1",
+ TD: "+235",
+ TG: "+228",
+ TH: "+66",
+ TL: "+670",
+ TM: "+993",
+ TN: "+216",
+ TO: "+676",
+ TR: "+90",
+ TT: "+1",
+ TV: "+688",
+ TW: "+886",
+ UA: "+380",
+ UG: "+256",
+ US: "+1",
+ UY: "+598",
+ VC: "+1",
+ VE: "+58",
+ VG: "+1",
+ VI: "+1",
+ VN: "+84",
+ VU: "+678",
+ WF: "+681",
+ WS: "+685",
+ YE: "+967",
+ ZA: "+27",
+ ZM: "+260",
+ ZW: "+263",
+} as const;
+
+// Helper function to convert country code to flag emoji
+export const getCountryFlag = (countryCode: string) => {
+ const codePoints = countryCode
+ .toUpperCase()
+ .split("")
+ .map((char) => 127397 + char.charCodeAt(0));
+
+ return String.fromCodePoint(...codePoints);
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/page.tsx
new file mode 100644
index 00000000000..5e3f3b942c9
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/configuration/page.tsx
@@ -0,0 +1,54 @@
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import { getTeamBySlug } from "@/api/team/get-team";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
+import { loginRedirect } from "@/utils/redirects";
+import { getSMSCountryTiers } from "./api/sms";
+import { InAppWalletSettingsPage } from "./components";
+
+export default async function Page(props: {
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const { team_slug, project_slug } = await props.params;
+
+ const [team, project, smsCountryTiers, authToken] = await Promise.all([
+ getTeamBySlug(team_slug),
+ getProject(team_slug, project_slug),
+ getSMSCountryTiers(),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(
+ `/team/${team_slug}/${project_slug}/wallets/user-wallets/configuration`,
+ );
+ }
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ if (!project) {
+ redirect(`/team/${team_slug}`);
+ }
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: team.id,
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/layout.tsx
new file mode 100644
index 00000000000..ad7fe93b277
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/layout.tsx
@@ -0,0 +1,27 @@
+import { TabPathLinks } from "@/components/ui/tabs";
+
+export default async function Layout(props: {
+ children: React.ReactNode;
+ params: Promise<{ team_slug: string; project_slug: string }>;
+}) {
+ const params = await props.params;
+ const basePath = `/team/${params.team_slug}/${params.project_slug}/wallets/user-wallets`;
+
+ return (
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/overview/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/overview/page.tsx
new file mode 100644
index 00000000000..db68b570f63
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/overview/page.tsx
@@ -0,0 +1,81 @@
+import { redirect } from "next/navigation";
+import { ResponsiveSearchParamsProvider } from "responsive-rsc";
+import { getAuthToken } from "@/api/auth-token";
+import { getProject } from "@/api/project/projects";
+import type { DurationId } from "@/components/analytics/date-range-selector";
+import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
+import { InAppWalletUsersPageContent } from "@/components/in-app-wallet-users-content/in-app-wallet-users-content";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { getFiltersFromSearchParams } from "@/lib/time";
+import { loginRedirect } from "@/utils/redirects";
+import { InAppWalletAnalytics } from "../../analytics/chart";
+import { InAppWalletsSummary } from "../../analytics/chart/Summary";
+
+export const dynamic = "force-dynamic";
+
+export default async function Page(props: {
+ params: Promise<{ team_slug: string; project_slug: string }>;
+ searchParams: Promise<{
+ from?: string;
+ to?: string;
+ type?: string;
+ interval?: string;
+ }>;
+}) {
+ const [searchParams, params] = await Promise.all([
+ props.searchParams,
+ props.params,
+ ]);
+
+ const authToken = await getAuthToken();
+ if (!authToken) {
+ loginRedirect(
+ `/team/${params.team_slug}/${params.project_slug}/wallets/user-wallets/overview`,
+ );
+ }
+
+ const defaultRange: DurationId = "last-30";
+ const { range, interval } = getFiltersFromSearchParams({
+ defaultRange,
+ from: searchParams.from,
+ interval: searchParams.interval,
+ to: searchParams.to,
+ });
+
+ const project = await getProject(params.team_slug, params.project_slug);
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
+ }
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: project.teamId,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/page.tsx
new file mode 100644
index 00000000000..8767a96f461
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/user-wallets/page.tsx
@@ -0,0 +1,10 @@
+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`,
+ );
+}