Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca65b43566 | ||
|
|
1f72396d7f | ||
|
|
08834f6385 | ||
|
|
143ab6d127 | ||
|
|
bc512551ab | ||
|
|
8e29d48426 | ||
|
|
e7e6446e7d | ||
|
|
7113f7c669 | ||
|
|
f9d7935d84 | ||
|
|
acd5656d4d | ||
|
|
6d1d344c48 | ||
|
|
26b8c320ae | ||
|
|
c7f7f48222 | ||
|
|
e814248d9c | ||
|
|
d7a0613067 | ||
|
|
dbc031664d | ||
|
|
5640d48854 | ||
|
|
9d6739ea59 | ||
|
|
dc49683357 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
|
||||
"extends": "//"
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
@@ -30,7 +31,7 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.1.0",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
@@ -107,7 +108,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"checkly": "5.6.0",
|
||||
"checkly": "6.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
|
||||
@@ -106,9 +106,27 @@ const projectPageRedirects = [
|
||||
source: `${projectRoute}/nebula/:path*`,
|
||||
},
|
||||
{
|
||||
source: `${projectRoute}/connect/analytics`,
|
||||
destination: `${projectRoute}`,
|
||||
permanent: false,
|
||||
source: `${projectRoute}/connect/analytics`,
|
||||
},
|
||||
];
|
||||
|
||||
const teamPageRedirects = [
|
||||
{
|
||||
destination: "/team/:team_slug/~/billing/:path*",
|
||||
permanent: false,
|
||||
source: "/team/:team_slug/~/settings/billing/:path*",
|
||||
},
|
||||
{
|
||||
destination: "/team/:team_slug/~/billing/invoices/:path*",
|
||||
permanent: false,
|
||||
source: "/team/:team_slug/~/settings/invoices/:path*",
|
||||
},
|
||||
{
|
||||
destination: "/team/:team_slug/~/billing",
|
||||
permanent: false,
|
||||
source: "/team/:team_slug/~/settings/credits",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -426,6 +444,7 @@ async function redirects() {
|
||||
},
|
||||
...legacyDashboardToTeamRedirects,
|
||||
...projectPageRedirects,
|
||||
...teamPageRedirects,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { normalizeTime } from "@/lib/time";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DateRangeSelector(props: {
|
||||
range: Range;
|
||||
setRange: (range: Range) => void;
|
||||
popoverAlign?: "start" | "end" | "center";
|
||||
className?: string;
|
||||
}) {
|
||||
const { range, setRange } = props;
|
||||
const daysDiff = differenceInCalendarDays(range.to, range.from);
|
||||
@@ -26,7 +28,7 @@ export function DateRangeSelector(props: {
|
||||
|
||||
return (
|
||||
<DatePickerWithRange
|
||||
className="w-auto bg-card"
|
||||
className={cn("w-auto bg-card", props.className)}
|
||||
from={range.from}
|
||||
header={
|
||||
<div className="mb-2 border-border border-b p-4">
|
||||
|
||||
@@ -76,8 +76,9 @@ export function RangeSelector({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-4">
|
||||
<div className="flex justify-end gap-3 flex-col lg:flex-row">
|
||||
<DateRangeSelector
|
||||
className="rounded-full"
|
||||
range={localRange}
|
||||
setRange={(newRange) => {
|
||||
setRange(newRange);
|
||||
@@ -89,7 +90,7 @@ export function RangeSelector({
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
className="bg-card"
|
||||
className="bg-card rounded-full"
|
||||
intervalType={localInterval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setInterval(newInterval);
|
||||
|
||||
@@ -82,7 +82,7 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
|
||||
|
||||
<Button asChild className="w-full gap-2 bg-card" variant="outline">
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
|
||||
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -60,7 +60,7 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
|
||||
You have outstanding invoices. Please{" "}
|
||||
<Link
|
||||
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
|
||||
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
|
||||
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
|
||||
>
|
||||
pay them
|
||||
</Link>{" "}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Button asChild className="justify-start gap-2" size="sm">
|
||||
<Link
|
||||
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
|
||||
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function MobileSidebar(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function useActiveSidebarLink(links: SidebarLink[]) {
|
||||
function useActiveSidebarLink(links: SidebarLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
"use client";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { AppFooter } from "../footers/app-footer";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
import { CustomSidebar, type SidebarLink } from "./Sidebar";
|
||||
|
||||
export function SidebarLayout(props: {
|
||||
@@ -51,129 +34,3 @@ export function SidebarLayout(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullWidthSidebarLayout(props: {
|
||||
contentSidebarLinks: SidebarLink[];
|
||||
footerSidebarLinks?: SidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar className="pt-2" collapsible="icon" side="left">
|
||||
<SidebarContent className="p-2">
|
||||
<RenderSidebarMenu links={contentSidebarLinks} />
|
||||
</SidebarContent>
|
||||
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3">
|
||||
<RenderSidebarMenu links={footerSidebarLinks} />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* right - content */}
|
||||
<div className="flex h-full flex-grow flex-col overflow-y-auto">
|
||||
<MobileSidebarTrigger
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarGroup(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
groupName: string;
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarMenuItem>
|
||||
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarMenu links={props.sidebarLinks} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarMenuItem>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarMenu(props: { links: SidebarLink[] }) {
|
||||
const sidebar = useSidebar();
|
||||
return (
|
||||
<SidebarMenu className="gap-1.5">
|
||||
{props.links.map((link, idx) => {
|
||||
// link
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuSubItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
activeClassName="text-foreground bg-accent"
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
isActive={link.isActive}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
// separator
|
||||
if ("separator" in link) {
|
||||
return (
|
||||
<SidebarSeparator
|
||||
className="my-1"
|
||||
key={`separator-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
|
||||
idx
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// group
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
groupName={link.group}
|
||||
key={link.group}
|
||||
sidebarLinks={link.links}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator className="h-4" orientation="vertical" />
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function TeamPlanBadge(props: {
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`);
|
||||
router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { NavLink } from "@/components/ui/NavLink";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ShadcnSidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type ShadcnSidebarLink =
|
||||
| ShadcnSidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: ShadcnSidebarBaseLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
}
|
||||
| {
|
||||
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
|
||||
links: Omit<ShadcnSidebarBaseLink, "icon">[];
|
||||
};
|
||||
|
||||
export function FullWidthSidebarLayout(props: {
|
||||
contentSidebarLinks: ShadcnSidebarLink[];
|
||||
footerSidebarLinks?: ShadcnSidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar className="pt-2" collapsible="icon" side="left">
|
||||
<SidebarContent className="p-2">
|
||||
<RenderSidebarMenu links={contentSidebarLinks} />
|
||||
</SidebarContent>
|
||||
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3">
|
||||
<RenderSidebarMenu links={footerSidebarLinks} />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* right - content */}
|
||||
<div className="flex h-full flex-grow flex-col overflow-y-auto">
|
||||
<MobileSidebarTrigger
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator
|
||||
className="h-4 bg-muted-foreground/50"
|
||||
orientation="vertical"
|
||||
/>
|
||||
{parentSubNav && "subMenu" in parentSubNav && (
|
||||
<>
|
||||
<span className="text-sm">{parentSubNav.subMenu.label}</span>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
|
||||
</>
|
||||
)}
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: ShadcnSidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isSubnavActive = useMemo(() => {
|
||||
function isActive(link: ShadcnSidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
return links.some(isActive);
|
||||
}, [links, pathname]);
|
||||
|
||||
return isSubnavActive;
|
||||
}
|
||||
|
||||
function RenderSidebarGroup(props: {
|
||||
sidebarLinks: ShadcnSidebarLink[];
|
||||
groupName: string;
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarMenuItem>
|
||||
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarMenu links={props.sidebarLinks} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarMenuItem>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarSubmenu(props: {
|
||||
links: ShadcnSidebarBaseLink[];
|
||||
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
|
||||
}) {
|
||||
const sidebar = useSidebar();
|
||||
const isSubnavActive = useIsSubnavActive(props.links);
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<DynamicHeight transition="height 200ms ease">
|
||||
<Collapsible className="group/collapsible" defaultOpen={isSubnavActive}>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{props.subMenu.icon && (
|
||||
<props.subMenu.icon className="size-4" />
|
||||
)}
|
||||
<span>{props.subMenu.label}</span>
|
||||
<ChevronDownIcon className="ml-auto group-[[data-state=open]]/collapsible:rotate-180 transition-transform" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="mt-0.5">
|
||||
<SidebarMenuSub className="pr-0 pl-2">
|
||||
{props.links.map((link) => {
|
||||
return (
|
||||
<SidebarMenuSubItem key={link.href}>
|
||||
<NavLink
|
||||
activeClassName="text-foreground bg-accent "
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent hover:text-foreground px-2 py-1.5 rounded-lg w-full"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
isActive={link.isActive}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span className="whitespace-nowrap">{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</DynamicHeight>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) {
|
||||
const sidebar = useSidebar();
|
||||
return (
|
||||
<SidebarMenu className="gap-1.5">
|
||||
{props.links.map((link, idx) => {
|
||||
// link
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuSubItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
activeClassName="text-foreground bg-accent"
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
isActive={link.isActive}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
// separator
|
||||
if ("separator" in link) {
|
||||
return (
|
||||
<SidebarSeparator
|
||||
className="my-1"
|
||||
key={`separator-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
|
||||
idx
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// subnav
|
||||
if ("subMenu" in link) {
|
||||
return (
|
||||
<RenderSidebarSubmenu links={link.links} subMenu={link.subMenu} />
|
||||
);
|
||||
}
|
||||
|
||||
// group
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
groupName={link.group}
|
||||
key={link.group}
|
||||
sidebarLinks={link.links}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const PaginationButtons = (props: {
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
disabled={activePage === 1}
|
||||
onClick={() => {
|
||||
setPage(activePage - 1);
|
||||
@@ -57,7 +57,7 @@ export const PaginationButtons = (props: {
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
disabled={activePage === totalPages}
|
||||
onClick={() => {
|
||||
setPage(activePage + 1);
|
||||
@@ -78,7 +78,7 @@ export const PaginationButtons = (props: {
|
||||
{pages.map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
isActive={activePage === page}
|
||||
onClick={() => {
|
||||
setPage(page);
|
||||
@@ -98,7 +98,7 @@ export const PaginationButtons = (props: {
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
disabled={activePage === 1}
|
||||
onClick={() => {
|
||||
setPage(activePage - 1);
|
||||
@@ -111,7 +111,7 @@ export const PaginationButtons = (props: {
|
||||
<>
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
}}
|
||||
@@ -129,7 +129,7 @@ export const PaginationButtons = (props: {
|
||||
{activePage - 1 > 0 && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
onClick={() => {
|
||||
setPage(activePage - 1);
|
||||
}}
|
||||
@@ -140,7 +140,7 @@ export const PaginationButtons = (props: {
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink className="rounded-full" isActive>
|
||||
<PaginationLink className="rounded-full lg:min-w-[38px]" isActive>
|
||||
{activePage}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
@@ -148,7 +148,7 @@ export const PaginationButtons = (props: {
|
||||
{activePage + 1 <= totalPages && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
onClick={() => {
|
||||
setPage(activePage + 1);
|
||||
}}
|
||||
@@ -167,7 +167,7 @@ export const PaginationButtons = (props: {
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
onClick={() => {
|
||||
setPage(totalPages);
|
||||
}}
|
||||
@@ -180,7 +180,7 @@ export const PaginationButtons = (props: {
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className="rounded-full"
|
||||
className="rounded-full lg:min-w-[38px]"
|
||||
disabled={activePage === totalPages}
|
||||
onClick={() => {
|
||||
setPage(activePage + 1);
|
||||
|
||||
@@ -138,7 +138,7 @@ export function UpsellContent(props: {
|
||||
<div className="flex flex-col gap-3 pt-4 sm:flex-row">
|
||||
<Button asChild className="flex-1 py-3 font-semibold" size="lg">
|
||||
<Link
|
||||
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
|
||||
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
|
||||
>
|
||||
<CrownIcon className="mr-2 h-4 w-4" />
|
||||
Upgrade to{" "}
|
||||
@@ -146,9 +146,7 @@ export function UpsellContent(props: {
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild className="md:flex-1" size="lg" variant="outline">
|
||||
<Link
|
||||
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
|
||||
>
|
||||
<Link href={`/team/${props.teamSlug}/~/billing?showPlans=true`}>
|
||||
View All Plans
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
@@ -50,9 +50,10 @@ const PaginationLink = ({
|
||||
<Button
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
"h-auto px-3 py-2",
|
||||
"h-auto px-3 py-2 border border-transparent",
|
||||
className,
|
||||
props.disabled && "cursor-not-allowed",
|
||||
!isActive && "border-border",
|
||||
)}
|
||||
variant={!isActive ? "outline" : "default"}
|
||||
{...props}
|
||||
|
||||
@@ -6,13 +6,6 @@ import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
|
||||
export const accountPlan = {
|
||||
enterprise: "enterprise",
|
||||
free: "free",
|
||||
growth: "growth",
|
||||
pro: "pro",
|
||||
} as const;
|
||||
|
||||
export type AuthorizedWallet = {
|
||||
id: string;
|
||||
accountId: string;
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { SectionTitle } from "../server/SectionTitle";
|
||||
|
||||
export default function NextSteps(props: { chain: ChainMetadata }) {
|
||||
const { chain } = props;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Next Steps" />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
|
||||
<FileTextIcon className="mt-0.5 size-5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={
|
||||
"https://blog.thirdweb.com/supercharge-user-adoption-integrate-embedded-wallets-in-minutes/"
|
||||
}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Create a login for {chain.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Supercharge User Adoption—Integrate In-App Wallets in Minutes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function ClaimChainSection() {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Claim Chain" />
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<p className="mb-1"> This chain page is unclaimed </p>
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Are you the owner of this ecosystem? Claim this page to add additional
|
||||
information on this page to help your developers get started!
|
||||
</p>
|
||||
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
className="gap-2"
|
||||
href="https://share.hsforms.com/1qAViW3U5SoKbabNhRZy4Kgea58c"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Claim Chain</span>
|
||||
<ExternalLinkIcon className="-mt-[3px] size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,7 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getRawAccount } from "../../../../account/settings/getAccount";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { BuyFundsSection } from "./components/client/BuyFundsSection";
|
||||
import NextSteps from "./components/client/NextSteps";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
import { ClaimChainSection } from "./components/server/ClaimChainSection";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
import { ExplorersSection } from "./components/server/explorer-section";
|
||||
import { FaucetSection } from "./components/server/FaucetSection";
|
||||
@@ -60,11 +58,6 @@ export default async function Page(props: {
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
{/*Next Steps */}
|
||||
<NextSteps chain={chain} />
|
||||
|
||||
{/* Claim Chain */}
|
||||
{!chainMetadata && <ClaimChainSection />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { isOwnerSupported, owner } from "thirdweb/extensions/common";
|
||||
import {
|
||||
getRoleMember,
|
||||
isGetRoleAdminSupported,
|
||||
} from "thirdweb/extensions/permissions";
|
||||
|
||||
export async function getContractCreator(
|
||||
contract: ThirdwebContract,
|
||||
functionSelectors: string[],
|
||||
) {
|
||||
if (isOwnerSupported(functionSelectors)) {
|
||||
return owner({
|
||||
contract,
|
||||
});
|
||||
}
|
||||
|
||||
if (isGetRoleAdminSupported(functionSelectors)) {
|
||||
return getRoleMember({
|
||||
contract,
|
||||
index: BigInt(0),
|
||||
role: "admin",
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { BadgeContainer } from "../../../../../../../../@/storybook/utils";
|
||||
import { BadgeContainer } from "@/storybook/utils";
|
||||
import { SupplyClaimedProgress } from "./supply-claimed-progress";
|
||||
|
||||
const meta = {
|
||||
@@ -19,7 +18,7 @@ function StoryVariants() {
|
||||
return (
|
||||
<div className="container max-w-md space-y-10 py-10">
|
||||
<BadgeContainer label="10 / Unlimited Supply">
|
||||
<SupplyClaimedProgress claimedSupply={10n} totalSupply={maxUint256} />
|
||||
<SupplyClaimedProgress claimedSupply={10n} totalSupply="unlimited" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="500/1000 Supply">
|
||||
|
||||
+2
-3
@@ -1,14 +1,13 @@
|
||||
import { InfinityIcon } from "lucide-react";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { supplyFormatter } from "../nft/format";
|
||||
|
||||
export function SupplyClaimedProgress(props: {
|
||||
claimedSupply: bigint;
|
||||
totalSupply: bigint;
|
||||
totalSupply: bigint | "unlimited";
|
||||
}) {
|
||||
// if total supply is unlimited
|
||||
if (props.totalSupply === maxUint256) {
|
||||
if (props.totalSupply === "unlimited") {
|
||||
return (
|
||||
<p className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium text-sm">Claimed Supply </span>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
|
||||
export function ContractCreatorBadge(props: {
|
||||
contractCreator: string;
|
||||
clientContract: ThirdwebContract;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 bg-card rounded-full px-2.5 py-1.5 border hover:bg-accent">
|
||||
<span className="text-xs text-foreground">By</span>
|
||||
<WalletAddress
|
||||
address={props.contractCreator}
|
||||
className="py-0 text-xs h-auto !no-underline"
|
||||
client={props.clientContract.client}
|
||||
iconClassName="size-3.5 hidden"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+25
@@ -88,6 +88,25 @@ export const WithImageAndMultipleSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: mockTokenImage,
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
discord: mockSocialUrls.discord,
|
||||
github: mockSocialUrls.github,
|
||||
telegram: mockSocialUrls.telegram,
|
||||
twitter: mockSocialUrls.twitter,
|
||||
website: mockSocialUrls.website,
|
||||
},
|
||||
symbol: "SMPL",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithContractCreator: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: "0x1234567890123456789012345678901234567890",
|
||||
image: mockTokenImage,
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
@@ -105,6 +124,7 @@ export const WithBrokenImageAndSingleSocialUrl: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: "broken-image.png",
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
@@ -118,6 +138,7 @@ export const WithoutImageAndNoSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: undefined,
|
||||
name: "Sample Token",
|
||||
socialUrls: {},
|
||||
@@ -129,6 +150,7 @@ export const LongNameAndLotsOfSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: "https://thirdweb.com/chain-icons/ethereum.svg",
|
||||
name: "This is a very long token name that should wrap to multiple lines",
|
||||
socialUrls: {
|
||||
@@ -148,6 +170,7 @@ export const AllSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: "https://thirdweb.com/chain-icons/ethereum.svg",
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
@@ -171,6 +194,7 @@ export const InvalidSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: "https://thirdweb.com/chain-icons/ethereum.svg",
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
@@ -188,6 +212,7 @@ export const SomeSocialUrls: Story = {
|
||||
args: {
|
||||
chainMetadata: ethereumChainMetadata,
|
||||
clientContract: mockContract,
|
||||
contractCreator: null,
|
||||
image: "https://thirdweb.com/chain-icons/ethereum.svg",
|
||||
name: "Sample Token",
|
||||
socialUrls: {
|
||||
|
||||
+9
@@ -19,6 +19,7 @@ import { YoutubeIcon } from "@/icons/brand-icons/YoutubeIcon";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { ContractCreatorBadge } from "./ContractCreatorBadge";
|
||||
|
||||
const platformToIcons: Record<string, React.FC<{ className?: string }>> = {
|
||||
discord: DiscordIcon,
|
||||
@@ -42,6 +43,7 @@ export function ContractHeaderUI(props: {
|
||||
clientContract: ThirdwebContract;
|
||||
socialUrls: object;
|
||||
imageClassName?: string;
|
||||
contractCreator: string | null;
|
||||
}) {
|
||||
const socialUrls = useMemo(() => {
|
||||
const socialUrlsValue: { name: string; href: string }[] = [];
|
||||
@@ -147,6 +149,13 @@ export function ContractHeaderUI(props: {
|
||||
|
||||
{/* bottom row */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-2">
|
||||
{props.contractCreator && (
|
||||
<ContractCreatorBadge
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={props.contractCreator}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToolTipLabel
|
||||
contentClassName="max-w-[300px]"
|
||||
label={
|
||||
|
||||
+26
-24
@@ -2,45 +2,47 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
import { getSDKTheme } from "../../../../../../../../../@/utils/sdk-component-theme";
|
||||
import { BuyWidget } from "thirdweb/react";
|
||||
import {
|
||||
reportAssetBuyFailed,
|
||||
reportAssetBuySuccessful,
|
||||
} from "@/analytics/report";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
|
||||
export function BuyTokenEmbed(props: {
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
tokenSymbol: string;
|
||||
tokenName: string;
|
||||
tokenAddress: string;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<PayEmbed
|
||||
<BuyWidget
|
||||
amount="1"
|
||||
chain={props.chain}
|
||||
className="!rounded-xl !w-full"
|
||||
client={props.client}
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
}}
|
||||
payOptions={{
|
||||
metadata: {
|
||||
name: `Buy ${props.tokenSymbol}`,
|
||||
},
|
||||
mode: "fund_wallet",
|
||||
prefillBuy: {
|
||||
allowEdits: {
|
||||
amount: true,
|
||||
chain: false,
|
||||
token: false,
|
||||
},
|
||||
amount: "1",
|
||||
chain: props.chain,
|
||||
token: {
|
||||
address: props.tokenAddress,
|
||||
name: props.tokenName,
|
||||
symbol: props.tokenSymbol,
|
||||
},
|
||||
},
|
||||
onError={(e) => {
|
||||
const errorMessage = parseError(e);
|
||||
reportAssetBuyFailed({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
});
|
||||
}}
|
||||
onSuccess={() => {
|
||||
reportAssetBuySuccessful({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: "DropERC20",
|
||||
});
|
||||
}}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
tokenAddress={props.tokenAddress as `0x${string}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-4
@@ -26,7 +26,7 @@ import {
|
||||
getApprovalForTransaction,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import { useActiveAccount, useSendTransaction } from "thirdweb/react";
|
||||
import { getClaimParams } from "thirdweb/utils";
|
||||
import { getClaimParams, maxUint256 } from "thirdweb/utils";
|
||||
import {
|
||||
reportAssetBuyFailed,
|
||||
reportAssetBuySuccessful,
|
||||
@@ -328,9 +328,16 @@ export function TokenDropClaim(props: {
|
||||
claimedSupply={BigInt(
|
||||
toTokens(props.claimCondition.supplyClaimed, props.decimals),
|
||||
)}
|
||||
totalSupply={BigInt(
|
||||
toTokens(props.claimCondition.maxClaimableSupply, props.decimals),
|
||||
)}
|
||||
totalSupply={
|
||||
props.claimCondition.maxClaimableSupply === maxUint256
|
||||
? "unlimited"
|
||||
: BigInt(
|
||||
toTokens(
|
||||
props.claimCondition.maxClaimableSupply,
|
||||
props.decimals,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export function ContractAnalyticsOverview(props: {
|
||||
</div>
|
||||
<ThirdwebAreaChart
|
||||
cardContentClassName="p-0"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[3]"
|
||||
chartClassName="aspect-[1.2] lg:aspect-[2.6]"
|
||||
className="border-none bg-background p-0"
|
||||
config={{
|
||||
events: {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
|
||||
export async function fetchTokenInfoFromBridge(params: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
clientId: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}&clientId=${params.clientId}`,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
data: Array<{
|
||||
iconUri: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
name: string;
|
||||
symbol: string;
|
||||
priceUsd: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
return data.data[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+40
-20
@@ -2,6 +2,8 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { decimals, getActiveClaimCondition } from "thirdweb/extensions/erc20";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { getContractCreator } from "../_components/getContractCreator";
|
||||
import { PageHeader } from "../_components/PageHeader";
|
||||
import { ContractHeaderUI } from "./_components/ContractHeader";
|
||||
import { TokenDropClaim } from "./_components/claim-tokens/claim-tokens-ui";
|
||||
@@ -9,6 +11,7 @@ import { ContractAnalyticsOverview } from "./_components/contract-analytics/cont
|
||||
import { BuyTokenEmbed } from "./_components/PayEmbedSection";
|
||||
import { TokenStats } from "./_components/PriceChart";
|
||||
import { RecentTransfers } from "./_components/RecentTransfers";
|
||||
import { fetchTokenInfoFromBridge } from "./_utils/fetch-coin-info";
|
||||
import { getCurrencyMeta } from "./_utils/getCurrencyMeta";
|
||||
|
||||
export async function ERC20PublicPage(props: {
|
||||
@@ -16,25 +19,43 @@ export async function ERC20PublicPage(props: {
|
||||
clientContract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}) {
|
||||
const [contractMetadata, activeClaimCondition, tokenDecimals] =
|
||||
await Promise.all([
|
||||
getContractMetadata({
|
||||
contract: props.serverContract,
|
||||
}),
|
||||
getActiveClaimConditionWithErrorHandler(props.serverContract),
|
||||
decimals({
|
||||
contract: props.serverContract,
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
contractMetadata,
|
||||
activeClaimCondition,
|
||||
tokenDecimals,
|
||||
tokenInfo,
|
||||
functionSelectors,
|
||||
] = await Promise.all([
|
||||
getContractMetadata({
|
||||
contract: props.serverContract,
|
||||
}),
|
||||
getActiveClaimConditionWithErrorHandler(props.serverContract),
|
||||
decimals({
|
||||
contract: props.serverContract,
|
||||
}),
|
||||
fetchTokenInfoFromBridge({
|
||||
chainId: props.serverContract.chain.id,
|
||||
clientId: props.clientContract.client.clientId,
|
||||
tokenAddress: props.serverContract.address,
|
||||
}),
|
||||
resolveFunctionSelectors(props.serverContract),
|
||||
]);
|
||||
|
||||
const claimConditionCurrencyMeta = activeClaimCondition
|
||||
? await getCurrencyMeta({
|
||||
chain: props.serverContract.chain,
|
||||
chainMetadata: props.chainMetadata,
|
||||
client: props.serverContract.client,
|
||||
currencyAddress: activeClaimCondition.currency,
|
||||
}).catch(() => undefined)
|
||||
: undefined;
|
||||
if (!contractMetadata.image && tokenInfo) {
|
||||
contractMetadata.image = tokenInfo.iconUri;
|
||||
}
|
||||
|
||||
const [contractCreator, claimConditionCurrencyMeta] = await Promise.all([
|
||||
getContractCreator(props.serverContract, functionSelectors),
|
||||
activeClaimCondition
|
||||
? getCurrencyMeta({
|
||||
chain: props.serverContract.chain,
|
||||
chainMetadata: props.chainMetadata,
|
||||
client: props.serverContract.client,
|
||||
currencyAddress: activeClaimCondition.currency,
|
||||
}).catch(() => undefined)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
const buyEmbed = (
|
||||
<BuyEmbed
|
||||
@@ -62,6 +83,7 @@ export async function ERC20PublicPage(props: {
|
||||
<ContractHeaderUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
image={contractMetadata.image}
|
||||
name={contractMetadata.name}
|
||||
socialUrls={
|
||||
@@ -141,8 +163,6 @@ function BuyEmbed(props: {
|
||||
chain={props.clientContract.chain}
|
||||
client={props.clientContract.client}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenName={props.tokenName}
|
||||
tokenSymbol={props.tokenSymbol}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ export function NFTPublicPageLayout(props: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
contractCreator: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
@@ -20,6 +21,7 @@ export function NFTPublicPageLayout(props: {
|
||||
<ContractHeaderUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={props.contractCreator}
|
||||
image={
|
||||
typeof props.contractMetadata.image === "string"
|
||||
? props.contractMetadata.image
|
||||
|
||||
+7
-3
@@ -6,6 +6,7 @@ import { ResponsiveLayout } from "@/components/blocks/Responsive";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getContractCreator } from "../_components/getContractCreator";
|
||||
import { NFTPublicPageLayout } from "./nft-page-layout";
|
||||
import {
|
||||
BuyNFTDropCardServer,
|
||||
@@ -35,15 +36,17 @@ export async function NFTPublicPage(props: {
|
||||
}),
|
||||
]);
|
||||
|
||||
const nftDropClaimParams =
|
||||
const [contractCreator, nftDropClaimParams] = await Promise.all([
|
||||
getContractCreator(props.serverContract, functionSelectors),
|
||||
props.type === "erc721"
|
||||
? await getNFTDropClaimParams({
|
||||
? getNFTDropClaimParams({
|
||||
chainMetadata: props.chainMetadata,
|
||||
functionSelectors,
|
||||
serverContract: props.serverContract,
|
||||
totalNFTCount,
|
||||
})
|
||||
: undefined;
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
const _isTokenByIndexSupported =
|
||||
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
|
||||
@@ -101,6 +104,7 @@ export async function NFTPublicPage(props: {
|
||||
<NFTPublicPageLayout
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
contractMetadata={contractMetadata}
|
||||
>
|
||||
<ResponsiveLayout
|
||||
|
||||
@@ -10,18 +10,18 @@ import type { ChainSupportedService } from "@/types/chain";
|
||||
|
||||
export const products = [
|
||||
{
|
||||
description: "Pre-built contracts & deployment tools",
|
||||
description: "Create, deploy and manage smart contracts",
|
||||
icon: ContractIcon,
|
||||
id: "contracts",
|
||||
link: "https://thirdweb.com/explore",
|
||||
name: "Contracts",
|
||||
},
|
||||
{
|
||||
description: "Client-side SDKs for wallets and blockchain interactions",
|
||||
description: "Create and manage crypto wallets",
|
||||
icon: ConnectSDKIcon,
|
||||
id: "connect-sdk",
|
||||
link: "https://thirdweb.com/connect",
|
||||
name: "Connect SDK",
|
||||
name: "Wallets",
|
||||
},
|
||||
{
|
||||
description: "Performant and scalable RPC service",
|
||||
@@ -35,24 +35,24 @@ export const products = [
|
||||
icon: EngineIcon,
|
||||
id: "engine",
|
||||
link: "https://thirdweb.com/engine",
|
||||
name: "Engine",
|
||||
name: "Transactions",
|
||||
},
|
||||
{
|
||||
description: "Account factory contracts, bundler & paymaster",
|
||||
description: "Enable gas sponsorship for seamless transactions",
|
||||
icon: SmartAccountIcon,
|
||||
id: "account-abstraction",
|
||||
link: "https://portal.thirdweb.com/connect/account-abstraction/overview",
|
||||
name: "Account Abstraction",
|
||||
},
|
||||
{
|
||||
description: "Point of sale solution for bridging, onramping & swapping",
|
||||
description: "Enable payments in any token on any chain",
|
||||
icon: PayIcon,
|
||||
id: "pay",
|
||||
link: "https://portal.thirdweb.com/connect/pay/overview",
|
||||
name: "Universal Bridge",
|
||||
name: "Payments",
|
||||
},
|
||||
{
|
||||
description: "Query, transform and analyze blockchain data",
|
||||
description: "Query and analyze blockchain data",
|
||||
icon: InsightIcon,
|
||||
id: "insight",
|
||||
link: "https://thirdweb.com/insight",
|
||||
|
||||
@@ -33,25 +33,25 @@ const HELP_PRODUCTS = [
|
||||
"https://playground.thirdweb.com/connect/sign-in/button?tab=code",
|
||||
},
|
||||
{
|
||||
description: "Wallets, auth, and onchain interactions",
|
||||
description: "Create and manage crypto wallets",
|
||||
icon: WalletIcon,
|
||||
title: "Connect",
|
||||
title: "Wallets",
|
||||
viewAllUrl: "https://portal.thirdweb.com/connect",
|
||||
},
|
||||
{
|
||||
description: "Bridge and onramp tokens on any chain",
|
||||
description: "Enable payments on any tokens on any chain",
|
||||
icon: PayIcon,
|
||||
title: "Universal Bridge",
|
||||
viewAllUrl: "https://portal.thirdweb.com/pay",
|
||||
title: "Payments",
|
||||
viewAllUrl: "https://portal.thirdweb.com/pay/troubleshoot",
|
||||
},
|
||||
{
|
||||
description: "Reliable transactions and monitoring",
|
||||
description: "Perform read and write transactions onchain",
|
||||
icon: EngineIcon,
|
||||
title: "Engine",
|
||||
viewAllUrl: "https://portal.thirdweb.com/engine/v3",
|
||||
title: "Transactions",
|
||||
viewAllUrl: "https://portal.thirdweb.com/engine/v3/troubleshoot",
|
||||
},
|
||||
{
|
||||
description: "Blockchain data queries and transformations",
|
||||
description: "Query and analyze blockchain data",
|
||||
icon: InsightIcon,
|
||||
title: "Insight",
|
||||
viewAllUrl: "https://portal.thirdweb.com/insight",
|
||||
@@ -117,7 +117,7 @@ export default async function SupportPage() {
|
||||
</header>
|
||||
</div>
|
||||
<section className="container flex flex-col gap-6">
|
||||
<h2 className="font-bold text-3xl">Learning Resources</h2>
|
||||
<h2 className="font-bold text-3xl">Support Articles</h2>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3">
|
||||
{HELP_PRODUCTS.map((product) => (
|
||||
<Card className="relative col-span-1 bg-card" key={product.title}>
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export function AccountButton(props: {
|
||||
className="justify-start px-3 text-muted-foreground text-sm hover:text-foreground"
|
||||
variant="ghost"
|
||||
>
|
||||
<Link href="/team/~/~/settings/billing">Manage Billing</Link>
|
||||
<Link href="/team/~/~/billing">Manage Billing</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
import {
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
ChartNoAxesColumnIcon,
|
||||
DatabaseIcon,
|
||||
DollarSignIcon,
|
||||
FileTextIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
WalletCardsIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
|
||||
export function TeamSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
ecosystems: Array<{ name: string; slug: string }>;
|
||||
}) {
|
||||
const { layoutPath, children } = props;
|
||||
|
||||
return (
|
||||
<FullWidthSidebarLayout
|
||||
contentSidebarLinks={[
|
||||
{
|
||||
exactMatch: true,
|
||||
href: layoutPath,
|
||||
icon: HomeIcon,
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/~/analytics`,
|
||||
icon: ChartNoAxesColumnIcon,
|
||||
label: "Analytics",
|
||||
},
|
||||
{
|
||||
links: [
|
||||
...props.ecosystems.map((ecosystem) => ({
|
||||
href: `${layoutPath}/~/ecosystem/${ecosystem.slug}`,
|
||||
label: ecosystem.name,
|
||||
})),
|
||||
{
|
||||
href: `${layoutPath}/~/ecosystem/create`,
|
||||
label: "Create Ecosystem",
|
||||
},
|
||||
],
|
||||
subMenu: {
|
||||
icon: WalletCardsIcon,
|
||||
label: "Ecosystems",
|
||||
},
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/~/audit-log`,
|
||||
icon: FileTextIcon,
|
||||
label: "Audit Log",
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/~/usage`,
|
||||
icon: DatabaseIcon,
|
||||
label: "Usage",
|
||||
},
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
href: `${layoutPath}/~/billing`,
|
||||
icon: DollarSignIcon,
|
||||
label: "Billing",
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/~/settings`,
|
||||
icon: SettingsIcon,
|
||||
label: "Team 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",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</FullWidthSidebarLayout>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -54,7 +54,7 @@ function BillingAlertBanner(props: {
|
||||
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
|
||||
)}
|
||||
>
|
||||
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
|
||||
<Link href={`/team/${props.teamSlug}/~/billing/invoices`}>
|
||||
{props.ctaLabel}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export function FreePlanUpsellBannerUI(props: {
|
||||
accentColor="green"
|
||||
cta={{
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
|
||||
link: `/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${
|
||||
props.highlightPlan || "growth"
|
||||
}`,
|
||||
text: "View plans",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function StaffModeNotice() {
|
||||
return (
|
||||
<div className="dark:bg-orange-800 bg-orange-200 border-b px-5">
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold text-lg tracking-tight">Staff Mode</p>
|
||||
<p className="text-sm text-foreground/80">
|
||||
You can only view this team, not take any actions.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
className="rounded-full gap-1.5"
|
||||
size="sm"
|
||||
variant="default"
|
||||
>
|
||||
<Link href="/team/~">
|
||||
Leave Staff Mode
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeamBySlug, getTeams } from "@/api/team";
|
||||
import { CustomChatButton } from "@/components/chat/CustomChatButton";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
import { StaffModeNotice } from "./_components/StaffModeNotice";
|
||||
import type { Ecosystem } from "./~/ecosystem/types";
|
||||
import { TeamSidebarLayout } from "./TeamSidebarLayout";
|
||||
|
||||
export default async function TeamLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -43,81 +44,66 @@ export default async function TeamLayout(props: {
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const ecosystems = await fetchEcosystemList(team.id, authToken);
|
||||
|
||||
const isStaffMode = !teams.some((t) => t.slug === team.slug);
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col">
|
||||
{!teams.some((t) => t.slug === team.slug) && (
|
||||
<div className="bg-warning-text">
|
||||
<div className="container flex items-center justify-between py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-bold text-white text-xl">👀 STAFF MODE 👀</p>
|
||||
<p className="text-sm text-white">
|
||||
You can only view this team, not take any actions.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="default">
|
||||
<Link href="/team/~">Leave Staff Mode</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<div className="flex h-dvh min-w-0 grow flex-col">
|
||||
{isStaffMode && <StaffModeNotice />}
|
||||
<AnnouncementBanner />
|
||||
<div className="bg-card border-b">
|
||||
<TeamHeaderLoggedIn
|
||||
account={account}
|
||||
accountAddress={accountAddress}
|
||||
client={client}
|
||||
currentProject={undefined}
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<AnnouncementBanner />
|
||||
<div className="bg-card">
|
||||
<TeamHeaderLoggedIn
|
||||
account={account}
|
||||
accountAddress={accountAddress}
|
||||
client={client}
|
||||
currentProject={undefined}
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
/>
|
||||
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
exactMatch: true,
|
||||
name: "Overview",
|
||||
path: `/team/${params.team_slug}`,
|
||||
},
|
||||
{
|
||||
name: "Analytics",
|
||||
path: `/team/${params.team_slug}/~/analytics`,
|
||||
},
|
||||
{
|
||||
name: "Ecosystems",
|
||||
path: `/team/${params.team_slug}/~/ecosystem`,
|
||||
},
|
||||
{
|
||||
name: "Usage",
|
||||
path: `/team/${params.team_slug}/~/usage`,
|
||||
},
|
||||
{
|
||||
name: "Audit Log",
|
||||
path: `/team/${params.team_slug}/~/audit-log`,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: `/team/${params.team_slug}/~/settings`,
|
||||
},
|
||||
]}
|
||||
tabContainerClassName="px-4 lg:px-6"
|
||||
/>
|
||||
<TeamSidebarLayout
|
||||
ecosystems={ecosystems.map((ecosystem) => ({
|
||||
name: ecosystem.name,
|
||||
slug: ecosystem.slug,
|
||||
}))}
|
||||
layoutPath={`/team/${params.team_slug}`}
|
||||
>
|
||||
{props.children}
|
||||
</TeamSidebarLayout>
|
||||
<div className="fixed right-6 bottom-6 z-50">
|
||||
<CustomChatButton
|
||||
authToken={authToken}
|
||||
clientId={undefined}
|
||||
examplePrompts={siwaExamplePrompts}
|
||||
isFloating={true}
|
||||
isLoggedIn={true}
|
||||
label="Ask AI Assistant"
|
||||
networks="all"
|
||||
pageType="support"
|
||||
teamId={team.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex grow flex-col">{props.children}</main>
|
||||
<div className="fixed right-6 bottom-6 z-50">
|
||||
<CustomChatButton
|
||||
authToken={authToken}
|
||||
clientId={undefined}
|
||||
examplePrompts={siwaExamplePrompts}
|
||||
isFloating={true}
|
||||
isLoggedIn={true}
|
||||
label="Ask AI Assistant"
|
||||
networks="all"
|
||||
pageType="support"
|
||||
teamId={team.id}
|
||||
/>
|
||||
</div>
|
||||
<AppFooter />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchEcosystemList(teamId: string, authToken: string) {
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/ecosystem-wallet`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await res.json()).result as Ecosystem[];
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default async function TeamOverviewPage(props: {
|
||||
title="Analytics"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16">
|
||||
<div className="flex grow flex-col justify-between gap-10 md:container md:max-w-7xl md:pt-8 md:pb-16">
|
||||
<div className="flex grow flex-col gap-6">
|
||||
<Suspense
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
|
||||
@@ -24,13 +24,15 @@ export default async function Layout(props: {
|
||||
>
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border border-b py-10">
|
||||
<div className="container flex flex-row justify-between">
|
||||
<div className="container max-w-5xl flex flex-row justify-between">
|
||||
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
|
||||
Audit Log
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{props.children}
|
||||
<div className="container max-w-5xl flex flex-col gap-8 pt-6 pb-10">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</UpsellWrapper>
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
{auditLogs.data.result.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-sm">No audit events found</p>
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ export function PlanInfoCardUI(props: {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Link href={`/team/${team.slug}/~/settings/invoices`}>
|
||||
<Link href={`/team/${team.slug}/~/billing/invoices`}>
|
||||
<FileTextIcon className="size-4 text-muted-foreground" />
|
||||
View Invoices
|
||||
</Link>
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
|
||||
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
|
||||
|
||||
const predefinedAmounts = [
|
||||
{ label: "$25", value: "25" },
|
||||
+1
-1
@@ -19,7 +19,7 @@ export default async function Page(props: {
|
||||
searchParamLoader(props.searchParams),
|
||||
]);
|
||||
|
||||
const pagePath = `/team/${params.team_slug}/settings/invoices`;
|
||||
const pagePath = `/team/${params.team_slug}/invoices`;
|
||||
|
||||
const account = await getValidAccount(pagePath);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { TabPathLinks } from "../../../../../../../@/components/ui/tabs";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
}>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [team, authToken] = await Promise.all([
|
||||
getTeamBySlug(params.team_slug),
|
||||
getAuthToken(),
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/~/settings`);
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border pt-10 pb-6">
|
||||
<div className="container max-w-6xl">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">Billing</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
exactMatch: true,
|
||||
name: "Overview",
|
||||
path: `/team/${params.team_slug}/~/billing`,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
path: `/team/${params.team_slug}/~/billing/invoices`,
|
||||
},
|
||||
]}
|
||||
scrollableClassName="container max-w-6xl"
|
||||
/>
|
||||
|
||||
<div className="container max-w-6xl flex flex-col grow gap-8 pt-6 pb-10">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { getTeamSubscriptions } from "@/api/team-subscription";
|
||||
import { CreditsInfoCard } from "@/components/billing/PlanCard";
|
||||
import { Coupons } from "@/components/billing/SubscriptionCoupons/Coupons";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getValidAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getValidAccount } from "../../../../../account/settings/getAccount";
|
||||
import { CreditBalanceSection } from "./components/credit-balance-section.client";
|
||||
import { PlanInfoCardClient } from "./components/PlanInfoCard.client";
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function Page(props: {
|
||||
props.params,
|
||||
props.searchParams,
|
||||
]);
|
||||
const pagePath = `/team/${params.team_slug}/settings/billing`;
|
||||
const pagePath = `/team/${params.team_slug}/billing`;
|
||||
|
||||
const account = await getValidAccount(pagePath);
|
||||
|
||||
@@ -64,7 +64,7 @@ export default async function Page(props: {
|
||||
team.billingStatus === "validPayment" || team.billingStatus === "pastDue";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<PlanInfoCardClient
|
||||
highlightPlan={highlightPlan}
|
||||
+15
-12
@@ -1,8 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getAuthToken } from "../../../../../../../../../../@/api/auth-token";
|
||||
import { fetchEcosystem } from "../../../utils/fetchEcosystem";
|
||||
import { EcosystemHeader } from "./ecosystem-header.client";
|
||||
|
||||
@@ -54,25 +54,28 @@ export async function EcosystemLayoutSlug({
|
||||
teamIdOrSlug={params.team_slug}
|
||||
/>
|
||||
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
<TabPathLinks
|
||||
className="pt-2"
|
||||
links={[
|
||||
{
|
||||
exactMatch: true,
|
||||
href: `${ecosystemLayoutPath}/${ecosystem.slug}`,
|
||||
label: "Overview",
|
||||
name: "Overview",
|
||||
path: `${ecosystemLayoutPath}/${ecosystem.slug}`,
|
||||
},
|
||||
{
|
||||
href: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
|
||||
label: "Analytics",
|
||||
name: "Analytics",
|
||||
path: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
|
||||
},
|
||||
{
|
||||
href: `${ecosystemLayoutPath}/${ecosystem.slug}#`,
|
||||
label: "Design (coming soon)",
|
||||
name: "Design (coming soon)",
|
||||
path: `${ecosystemLayoutPath}/${ecosystem.slug}/#`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
scrollableClassName="container max-w-7xl"
|
||||
/>
|
||||
<div className="container max-w-7xl flex flex-col grow pt-6 pb-10">
|
||||
{children}
|
||||
</SidebarLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-76
@@ -1,12 +1,5 @@
|
||||
"use client";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CheckIcon,
|
||||
ChevronsUpDownIcon,
|
||||
ExternalLinkIcon,
|
||||
PencilIcon,
|
||||
PlusCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { AlertTriangleIcon, ExternalLinkIcon, PencilIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -25,14 +18,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ImageUpload } from "@/components/ui/image-upload";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
@@ -41,7 +26,6 @@ import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
|
||||
import { useEcosystem } from "../hooks/use-ecosystem";
|
||||
@@ -78,56 +62,6 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
}
|
||||
}
|
||||
|
||||
function EcosystemSelect(props: {
|
||||
ecosystem: Ecosystem;
|
||||
ecosystemLayoutPath: string;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const { data: ecosystems, isPending } = useEcosystemList({
|
||||
teamIdOrSlug: props.teamIdOrSlug,
|
||||
});
|
||||
|
||||
return isPending ? (
|
||||
<Skeleton className="h-10 w-full md:w-[160px]" />
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="relative flex w-full justify-start truncate pr-8 pl-3 md:w-48"
|
||||
variant="outline"
|
||||
>
|
||||
<div className="truncate">{props.ecosystem?.name}</div>
|
||||
<ChevronsUpDownIcon className="absolute right-2 h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full md:w-48">
|
||||
<DropdownMenuGroup>
|
||||
{ecosystems?.map((ecosystem) => (
|
||||
<DropdownMenuItem asChild key={ecosystem.id}>
|
||||
<Link
|
||||
className="relative flex cursor-pointer items-center pr-3 pl-8"
|
||||
href={`${props.ecosystemLayoutPath}/${ecosystem.slug}`}
|
||||
>
|
||||
{ecosystem.slug === props.ecosystem.slug && (
|
||||
<CheckIcon className="absolute left-2 h-4 w-4 text-foreground" />
|
||||
)}
|
||||
<div className="truncate">{ecosystem.name}</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<Link className="" href={`${props.ecosystemLayoutPath}/create`}>
|
||||
<DropdownMenuItem className="relative flex cursor-pointer items-center pr-3 pl-8">
|
||||
<PlusCircleIcon className="absolute left-2 h-4 w-4" />
|
||||
<div className="truncate">New Ecosystem</div>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function EcosystemHeader(props: {
|
||||
ecosystem: Ecosystem;
|
||||
ecosystemLayoutPath: string;
|
||||
@@ -236,8 +170,8 @@ export function EcosystemHeader(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b py-8">
|
||||
<div className="container flex flex-col gap-8">
|
||||
<div className="pt-8 pb-4">
|
||||
<div className="container max-w-7xl flex flex-col gap-8">
|
||||
<EcosystemAlertBanner ecosystem={ecosystem} />
|
||||
<header className="flex flex-col gap-12">
|
||||
<div className="flex flex-col justify-between gap-4 md:grid-cols-4 md:flex-row">
|
||||
@@ -402,13 +336,6 @@ export function EcosystemHeader(props: {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-4 md:items-end">
|
||||
<EcosystemSelect
|
||||
ecosystem={ecosystem}
|
||||
ecosystemLayoutPath={props.ecosystemLayoutPath}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
+25
-3
@@ -1,3 +1,4 @@
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client";
|
||||
import { EcosystemWalletPricingCard } from "./components/pricing-card";
|
||||
|
||||
@@ -6,8 +7,29 @@ export async function EcosystemCreatePage(props: {
|
||||
teamId: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col items-center justify-center">
|
||||
<main className="container max-w-5xl py-20">
|
||||
<div>
|
||||
<div className="border-border py-8 border-b">
|
||||
<div className="container max-w-5xl">
|
||||
<h1 className="font-semibold text-3xl tracking-tight mb-1">
|
||||
Create Ecosystem
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Ecosystem Wallets enable your users to seamlessly access their
|
||||
assets across various apps and games within your ecosystem.
|
||||
<br /> You can control which apps join your ecosystem and how their
|
||||
users interact with your wallet.{" "}
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/connect/ecosystems/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col grow container max-w-5xl pb-20 pt-8">
|
||||
<div className="flex flex-col rounded-lg border bg-card lg:flex-row">
|
||||
<CreateEcosystemForm
|
||||
teamId={props.teamId}
|
||||
@@ -15,7 +37,7 @@ export async function EcosystemCreatePage(props: {
|
||||
/>
|
||||
<EcosystemWalletPricingCard />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-14
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -20,6 +19,7 @@ import { ImageUpload } from "@/components/ui/image-upload";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { createEcosystem } from "../../actions/create-ecosystem";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -87,15 +87,6 @@ export function CreateEcosystemForm(props: {
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-6 p-4 lg:p-6">
|
||||
<div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
Create Ecosystem
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Create wallets that work across every chain and every app
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -175,14 +166,13 @@ export function CreateEcosystemForm(props: {
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Link
|
||||
className="text-primary"
|
||||
href="https://portal.thirdweb.com/connect/ecosystems/ecosystem-permissions"
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/connect/wallet/ecosystem/permissions"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about ecosystem permissions
|
||||
</Link>
|
||||
</UnderlineLink>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
|
||||
export default function Loading() {
|
||||
return <GenericLoadingPage className="border-none" />;
|
||||
}
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export function useEcosystemList({ teamIdOrSlug }: { teamIdOrSlug: string }) {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "GET",
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error ?? "Failed to fetch ecosystems");
|
||||
}
|
||||
|
||||
const data = res.data as { result: Ecosystem[] };
|
||||
return data.result;
|
||||
},
|
||||
queryKey: ["ecosystems", teamIdOrSlug, address],
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
import { ArrowRightIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getAuthToken } from "../../../../../../../@/api/auth-token";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
import headerImage from "./assets/header.png";
|
||||
import { fetchEcosystemList } from "./utils/fetchEcosystemList";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -26,61 +21,10 @@ export default async function Page(props: {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
if (ecosystems[0]) {
|
||||
redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`);
|
||||
}
|
||||
|
||||
return <EcosystemLandingPage ecosystemLayoutPath={ecosystemLayoutPath} />;
|
||||
}
|
||||
|
||||
async function EcosystemLandingPage(props: { ecosystemLayoutPath: string }) {
|
||||
return (
|
||||
<div className="container flex grow flex-col items-center justify-center py-20">
|
||||
{/* Card */}
|
||||
<div className="flex max-w-lg flex-col rounded-lg border bg-card">
|
||||
<Image
|
||||
alt="Ecosystems"
|
||||
className="w-full border-b"
|
||||
sizes="100vw"
|
||||
src={headerImage}
|
||||
/>
|
||||
|
||||
{/* body */}
|
||||
<div className="px-4 py-6 lg:px-6">
|
||||
<h2 className="mb-2 text-balance font-semibold text-2xl text-foreground leading-tight tracking-tighter">
|
||||
One wallet, a whole ecosystem of apps and games
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
With Ecosystem Wallets, your users can access their assets across
|
||||
hundreds of apps and games within your ecosystem. You can control
|
||||
which apps join your ecosystem and how their users interact with
|
||||
your wallet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-6 flex flex-col justify-end gap-4 border-t px-4 py-6 lg:flex-row lg:px-6">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
className="flex flex-row gap-2"
|
||||
href="https://portal.thirdweb.com/connect/ecosystems/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild className="gap-2">
|
||||
<Link href={`${props.ecosystemLayoutPath}/create`}>
|
||||
Create Ecosystem
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
redirect(`${ecosystemLayoutPath}/create`);
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Account } from "@/hooks/useApi";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getTeamSettingsLinks } from "./_components/sidebar/getTeamSettingsLinks";
|
||||
import { TeamSettingsSidebar } from "./_components/sidebar/TeamSettingsSidebar";
|
||||
import { TeamSettingsMobileNav } from "./_components/sidebar/TeamsMobileNav";
|
||||
|
||||
// on the /~/settings page
|
||||
// - On desktop: show the general settings as usual
|
||||
// - On mobile: show the full nav instead of page content and when user clicks on the "General Settings" ( first link ) - hide the full nav and show the page content
|
||||
|
||||
export function SettingsLayout(props: {
|
||||
team: Team;
|
||||
children: React.ReactNode;
|
||||
account: Account;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const [_showFullNavOnMobile, setShowFullNavOnMobile] = useState(true);
|
||||
const pathname = usePathname();
|
||||
const isSettingsOverview = (pathname || "").endsWith("/~/settings");
|
||||
const showFullNavOnMobile = _showFullNavOnMobile && isSettingsOverview;
|
||||
const links = getTeamSettingsLinks(props.team.slug);
|
||||
const activeLink = links.find((link) => pathname === link.href);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
{/* Huge page title */}
|
||||
<div className="border-border border-b py-10">
|
||||
<div className="container">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Team Settings
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden">
|
||||
<TeamSettingsMobileNav
|
||||
activeLink={activeLink}
|
||||
setShowFull={setShowFullNavOnMobile}
|
||||
showFull={showFullNavOnMobile}
|
||||
teamSlug={props.team.slug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container flex grow gap-8 lg:min-h-[900px] [&>*]:py-8 lg:[&>*]:py-10">
|
||||
<TeamSettingsSidebar
|
||||
account={props.account}
|
||||
client={props.client}
|
||||
team={props.team}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex max-w-full grow flex-col",
|
||||
// if showing full nav on mobile - hide the page content
|
||||
showFullNavOnMobile && "max-sm:hidden",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -131,7 +131,7 @@ export function TeamDedicatedSupportCard(props: {
|
||||
label: "Upgrade Plan",
|
||||
onClick: () =>
|
||||
router.push(
|
||||
`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=scale`,
|
||||
`/team/${props.team.slug}/~/billing?showPlans=true&highlight=scale`,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export function TeamSettingsMobileNav(props: {
|
||||
|
||||
if (!showFull) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 border-border border-b px-4 py-4 text-muted-foreground">
|
||||
<div className="flex items-center gap-2 border-border border-b px-4 py-4 text-muted-foreground text-sm">
|
||||
<Link
|
||||
className="inline-flex items-center gap-1"
|
||||
href={teamLinks[0]?.href || "#"}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ApplyForOpCredits } from "@app/team/[team_slug]/(team)/~/settings/credits/components/ApplyForOpCreditsModal";
|
||||
import type { Team } from "@/api/team";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import type { Account } from "@/hooks/useApi";
|
||||
|
||||
export const SettingsGasCreditsPage = (props: {
|
||||
team: Team;
|
||||
account: Account;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="mb-0.5 font-semibold text-2xl tracking-tight">
|
||||
Credits
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Apply to the Optimism Superchain App Accelerator.{" "}
|
||||
<UnderlineLink
|
||||
href="https://blog.thirdweb.com/accelerating-the-superchain-with-optimism/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-6" />
|
||||
<ApplyForOpCredits account={props.account} team={props.team} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
import { Flex, FormControl } from "@chakra-ui/react";
|
||||
import { FormHelperText, FormLabel } from "chakra/form";
|
||||
import { Select as ChakraSelect } from "chakra-react-select";
|
||||
import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { Team } from "@/api/team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { Account } from "@/hooks/useApi";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import { PlanToCreditsRecord } from "./ApplyForOpCreditsModal";
|
||||
import { applyOpSponsorship } from "./applyOpSponsorship";
|
||||
|
||||
interface FormSchema {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
thirdweb_account_id: string;
|
||||
plan_type: string;
|
||||
email: string;
|
||||
company: string;
|
||||
website: string;
|
||||
twitterhandle: string;
|
||||
superchain_verticals: string;
|
||||
superchain_chain: string;
|
||||
what_would_you_like_to_meet_about_: string;
|
||||
}
|
||||
|
||||
interface ApplyForOpCreditsFormProps {
|
||||
onClose: () => void;
|
||||
plan: Team["billingPlan"];
|
||||
account: Account;
|
||||
}
|
||||
|
||||
export const ApplyForOpCreditsForm: React.FC<ApplyForOpCreditsFormProps> = ({
|
||||
onClose,
|
||||
account,
|
||||
plan,
|
||||
}) => {
|
||||
const [, setHasAppliedForOpGrant] = useLocalStorage(
|
||||
`appliedForOpGrant-${account?.id}`,
|
||||
false,
|
||||
);
|
||||
const transformedQueryData = useMemo(
|
||||
() => ({
|
||||
company: "",
|
||||
email: account?.email || "",
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
plan_type: PlanToCreditsRecord[plan].plan,
|
||||
superchain_chain: "",
|
||||
superchain_verticals: "",
|
||||
thirdweb_account_id: account?.id || "",
|
||||
twitterhandle: "",
|
||||
website: "",
|
||||
what_would_you_like_to_meet_about_: "",
|
||||
}),
|
||||
[account, plan],
|
||||
);
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
defaultValues: transformedQueryData,
|
||||
values: transformedQueryData,
|
||||
});
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"We have received your application and will notify you if you are selected.",
|
||||
"Something went wrong, please try again.",
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="form"
|
||||
direction="column"
|
||||
gap={4}
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
const fields = Object.keys(data).map((key) => ({
|
||||
name: key,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
value: (data as any)[key],
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await applyOpSponsorship({
|
||||
fields,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Form submission failed");
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
onClose();
|
||||
setHasAppliedForOpGrant(true);
|
||||
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Flex gap={4}>
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<Input {...form.register("firstname", { required: true })} />
|
||||
</FormControl>
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<Input {...form.register("lastname", { required: true })} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Company Name</FormLabel>
|
||||
<Input {...form.register("company", { required: true })} />
|
||||
</FormControl>
|
||||
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Company Website</FormLabel>
|
||||
<Input type="url" {...form.register("website", { required: true })} />
|
||||
<FormHelperText>URL should start with https://</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Company Social Account</FormLabel>
|
||||
<Input
|
||||
type="url"
|
||||
{...form.register("twitterhandle", { required: true })}
|
||||
/>
|
||||
<FormHelperText>URL should start with https://</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Industry</FormLabel>
|
||||
<ChakraSelect
|
||||
isRequired
|
||||
onChange={(value) => {
|
||||
if (value?.value) {
|
||||
form.setValue("superchain_verticals", value.value);
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
"DAOs",
|
||||
"Education & Community",
|
||||
"Fandom & Rewards",
|
||||
"Gaming & Metaverse",
|
||||
"Infra & Dev Tools",
|
||||
"NFTs",
|
||||
"Payments & Finance (DeFi)",
|
||||
"Security & Identity",
|
||||
"Social",
|
||||
"Other",
|
||||
].map((vertical) => ({
|
||||
label: vertical,
|
||||
value:
|
||||
vertical === "Payments & Finance (DeFi)" ? "DeFi" : vertical,
|
||||
}))}
|
||||
placeholder="Select industry"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={6} isRequired>
|
||||
<FormLabel>Chain</FormLabel>
|
||||
<ChakraSelect
|
||||
isMulti
|
||||
isRequired
|
||||
onChange={(values) => {
|
||||
form.setValue(
|
||||
"superchain_chain",
|
||||
values.map(({ value }) => value).join(";"),
|
||||
);
|
||||
}}
|
||||
options={[
|
||||
"Optimism",
|
||||
"Base",
|
||||
"Zora",
|
||||
"Mode",
|
||||
"Frax",
|
||||
"Cyber",
|
||||
"Redstone",
|
||||
"Ancient8",
|
||||
"Donatuz",
|
||||
"Mantle",
|
||||
"Soneium",
|
||||
"Lisk",
|
||||
"Arena-Z",
|
||||
"Superseed",
|
||||
"Ink",
|
||||
].map((chain) => ({
|
||||
label: chain === "Optimism" ? "OP Mainnet" : chain,
|
||||
value: chain,
|
||||
}))}
|
||||
placeholder="Select chains"
|
||||
selectedOptionStyle="check"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={6}>
|
||||
<FormLabel>Tell us more about your project</FormLabel>
|
||||
<Textarea
|
||||
{...form.register("what_would_you_like_to_meet_about_")}
|
||||
placeholder="Tell us more about your project -- the more you share, the easier you make the approval process."
|
||||
/>
|
||||
<FormHelperText>Minimum 150 characters recommended.</FormHelperText>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={form.formState.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{form.formState.isSubmitting ? "Applying..." : "Apply now"}
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { Team } from "@/api/team";
|
||||
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { type Account, accountPlan } from "@/hooks/useApi";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
|
||||
import { ApplyForOpCreditsForm } from "./ApplyForOpCreditsForm";
|
||||
import { PlanCard } from "./PlanCard";
|
||||
|
||||
export type CreditsRecord = {
|
||||
plan: Team["billingPlan"];
|
||||
upTo?: true;
|
||||
credits: string;
|
||||
features?: string[];
|
||||
};
|
||||
|
||||
const tier2Credits: Omit<CreditsRecord, "plan"> = {
|
||||
credits: "$2,500",
|
||||
features: [
|
||||
"10k monthly active wallets",
|
||||
"User analytics",
|
||||
"Custom Auth",
|
||||
"Custom Branding",
|
||||
],
|
||||
upTo: true,
|
||||
};
|
||||
|
||||
const tier1Credits: Omit<CreditsRecord, "plan"> = {
|
||||
credits: "$250",
|
||||
upTo: true,
|
||||
};
|
||||
|
||||
export const PlanToCreditsRecord: Record<Team["billingPlan"], CreditsRecord> = {
|
||||
accelerate: {
|
||||
plan: "accelerate",
|
||||
...tier2Credits,
|
||||
},
|
||||
free: {
|
||||
plan: "free",
|
||||
...tier1Credits,
|
||||
},
|
||||
|
||||
growth: {
|
||||
plan: "growth",
|
||||
...tier1Credits,
|
||||
},
|
||||
growth_legacy: {
|
||||
plan: "growth_legacy",
|
||||
...tier2Credits,
|
||||
},
|
||||
pro: {
|
||||
credits: "$3,000+",
|
||||
features: [
|
||||
"Custom rate limits for APIs & Infra",
|
||||
"Enterprise grade SLAs",
|
||||
"Dedicated support",
|
||||
],
|
||||
plan: "pro",
|
||||
upTo: true,
|
||||
},
|
||||
scale: {
|
||||
plan: "scale",
|
||||
...tier2Credits,
|
||||
},
|
||||
starter: {
|
||||
plan: "starter",
|
||||
...tier1Credits,
|
||||
},
|
||||
};
|
||||
|
||||
export function ApplyForOpCredits(props: { team: Team; account: Account }) {
|
||||
const { account, team } = props;
|
||||
const validTeamPlan = getValidTeamPlan(team);
|
||||
const hasValidPaymentMethod = validTeamPlan !== "free";
|
||||
|
||||
const [hasAppliedForOpGrant] = useLocalStorage(
|
||||
`appliedForOpGrant-${team.id}`,
|
||||
false,
|
||||
);
|
||||
|
||||
const isStarterPlan = validTeamPlan === "starter";
|
||||
const isProPlan = validTeamPlan === "pro";
|
||||
const creditsRecord = PlanToCreditsRecord[validTeamPlan];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{/* credits info */}
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="relative flex justify-between px-6 py-4">
|
||||
<h2 className="font-semibold text-lg capitalize tracking-tight">
|
||||
{creditsRecord.upTo && "Up to"} {creditsRecord.credits} Gas
|
||||
Credits
|
||||
</h2>
|
||||
<TeamPlanBadge
|
||||
className="absolute top-5 right-6"
|
||||
plan={creditsRecord.plan}
|
||||
teamSlug={team.slug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* alert */}
|
||||
{!hasValidPaymentMethod && (
|
||||
<div className="px-6 pb-6">
|
||||
<Alert className="bg-background" variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Payment method required</AlertTitle>
|
||||
<AlertDescription>
|
||||
You need to add a payment method to be able to claim credits.{" "}
|
||||
<br /> This is to prevent abuse, you will not be charged.{" "}
|
||||
<UnderlineLink
|
||||
href={`/team/${props.team.slug}/~/settings/billing`}
|
||||
>
|
||||
Upgrade plan to get started
|
||||
</UnderlineLink>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end border-t p-4">
|
||||
<ApplyOpCreditsButton
|
||||
account={account}
|
||||
hasAppliedForOpGrant={hasAppliedForOpGrant}
|
||||
hasValidPaymentMethod={hasValidPaymentMethod}
|
||||
validTeamPlan={validTeamPlan}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isProPlan && (
|
||||
<div>
|
||||
<p className="my-6 text-center text-muted-foreground text-sm tracking-wide">
|
||||
Or upgrade and get access to more credits
|
||||
</p>
|
||||
<div className="flex flex-col gap-6">
|
||||
{isStarterPlan && (
|
||||
<PlanCard
|
||||
creditsRecord={PlanToCreditsRecord[accountPlan.growth]}
|
||||
teamSlug={team.slug}
|
||||
/>
|
||||
)}
|
||||
<PlanCard
|
||||
creditsRecord={PlanToCreditsRecord[accountPlan.pro]}
|
||||
teamSlug={team.slug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-6 text-center text-muted-foreground text-sm">
|
||||
We are open to distributing more than the upper limit for each tier if
|
||||
you make a strong case about how it will be utilized.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyOpCreditsButton(props: {
|
||||
hasAppliedForOpGrant: boolean;
|
||||
hasValidPaymentMethod: boolean;
|
||||
validTeamPlan: Team["billingPlan"];
|
||||
account: Account;
|
||||
}) {
|
||||
const {
|
||||
hasAppliedForOpGrant,
|
||||
hasValidPaymentMethod,
|
||||
validTeamPlan,
|
||||
account,
|
||||
} = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={setIsOpen} open={isOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={!hasValidPaymentMethod || hasAppliedForOpGrant}
|
||||
size="sm"
|
||||
>
|
||||
{hasAppliedForOpGrant ? (
|
||||
"Already applied"
|
||||
) : (
|
||||
<>
|
||||
Apply Now
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="!max-w-2xl overflow-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Apply for OP credits</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-5" />
|
||||
<ApplyForOpCreditsForm
|
||||
account={account}
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
plan={validTeamPlan}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { CreditsRecord } from "./ApplyForOpCreditsModal";
|
||||
|
||||
type PlanCardProps = {
|
||||
creditsRecord: CreditsRecord;
|
||||
teamSlug: string;
|
||||
};
|
||||
|
||||
export function PlanCard({ creditsRecord, teamSlug }: PlanCardProps) {
|
||||
return (
|
||||
<div className="relative rounded-lg border bg-card">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<h2 className="font-semibold text-foreground text-lg tracking-tight">
|
||||
{creditsRecord.upTo || "Up To"} {creditsRecord.credits} Gas Credits
|
||||
</h2>
|
||||
<TeamPlanBadge plan={creditsRecord.plan} teamSlug={teamSlug} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 px-6 py-4">
|
||||
{creditsRecord.features && (
|
||||
<ul className="list-disc space-y-1 pl-4 text-muted-foreground text-sm">
|
||||
{creditsRecord.features.map((feature) => (
|
||||
<li className="text-muted-foreground" key={feature}>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end border-t px-6 py-4">
|
||||
<Button
|
||||
asChild
|
||||
className="gap-2 bg-background"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Link href={`/team/${teamSlug}/~/settings/billing`}>Upgrade</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
"use server";
|
||||
|
||||
export async function applyOpSponsorship(params: {
|
||||
fields: {
|
||||
name: string;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
value: any;
|
||||
}[];
|
||||
}) {
|
||||
const { fields } = params;
|
||||
|
||||
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
|
||||
return {
|
||||
error: "missing HUBSPOT_ACCESS_TOKEN",
|
||||
ok: false,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/2fbf6a3b-d4cc-4a23-a4f5-42674e8487b9",
|
||||
{
|
||||
body: JSON.stringify({ fields }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.text();
|
||||
return {
|
||||
error: errorMessage,
|
||||
ok: response.ok,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ChakraProviderSetup } from "chakra/ChakraProviderSetup";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getValidAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { SettingsGasCreditsPage } from "./SettingsCreditsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const account = await getValidAccount(
|
||||
`/team/${params.team_slug}/~/settings/credits`,
|
||||
);
|
||||
const team = await getTeamBySlug(params.team_slug);
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SettingsGasCreditsPage account={account} team={team} />
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getAuthToken } from "../../../../../../../@/api/auth-token";
|
||||
import { getValidAccount } from "../../../../../account/settings/getAccount";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
import { SettingsLayout } from "./SettingsLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
@@ -14,8 +12,7 @@ export default async function Layout(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [account, team, authToken] = await Promise.all([
|
||||
getValidAccount(`/team/${params.team_slug}/~/settings`),
|
||||
const [team, authToken] = await Promise.all([
|
||||
getTeamBySlug(params.team_slug),
|
||||
getAuthToken(),
|
||||
]);
|
||||
@@ -28,14 +25,38 @@ export default async function Layout(props: {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsLayout account={account} client={client} team={team}>
|
||||
{props.children}
|
||||
</SettingsLayout>
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border pt-10 pb-5">
|
||||
<div className="container max-w-6xl">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Team Settings
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
exactMatch: true,
|
||||
name: "General",
|
||||
path: `/team/${params.team_slug}/~/settings`,
|
||||
},
|
||||
{
|
||||
name: "Members",
|
||||
path: `/team/${params.team_slug}/~/settings/members`,
|
||||
},
|
||||
{
|
||||
name: "Notifications",
|
||||
path: `/team/${params.team_slug}/~/settings/notifications`,
|
||||
},
|
||||
]}
|
||||
scrollableClassName="container max-w-6xl"
|
||||
/>
|
||||
|
||||
<div className="container max-w-6xl flex flex-col grow gap-8 py-8">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ export function InviteSection(props: {
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link
|
||||
className="gap-2"
|
||||
href={`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=growth`}
|
||||
href={`/team/${props.team.slug}/~/billing?showPlans=true&highlight=growth`}
|
||||
>
|
||||
Upgrade
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabPathLinks } from "../../../../../../../@/components/ui/tabs";
|
||||
|
||||
export default async function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -9,53 +9,54 @@ export default async function Layout(props: {
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const usagePath = `/team/${params.team_slug}/~/usage`;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="border-border border-b py-10">
|
||||
<div className="container flex flex-row justify-between">
|
||||
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
|
||||
Usage
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/team/${params.team_slug}/~/settings/billing`}>
|
||||
<div className="border-border pt-10 pb-4">
|
||||
<div className="container max-w-7xl flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">Usage</h1>
|
||||
<div className="items-center gap-3 hidden lg:flex">
|
||||
<Button asChild className="rounded-full bg-card" variant="outline">
|
||||
<Link href={`/team/${params.team_slug}/~/billing`}>
|
||||
Billing Settings
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/team/${params.team_slug}/~/settings/invoices`}>
|
||||
<Button asChild className="rounded-full bg-card" variant="outline">
|
||||
<Link href={`/team/${params.team_slug}/~/billing/invoices`}>
|
||||
Invoice History
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
exactMatch: true,
|
||||
href: `/team/${params.team_slug}/~/usage`,
|
||||
label: "Overview",
|
||||
name: "Overview",
|
||||
path: usagePath,
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
href: `/team/${params.team_slug}/~/usage/rpc`,
|
||||
label: "RPC",
|
||||
name: "RPC",
|
||||
path: `${usagePath}/rpc`,
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
href: `/team/${params.team_slug}/~/usage/storage`,
|
||||
label: "Storage",
|
||||
name: "Storage",
|
||||
path: `${usagePath}/storage`,
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
href: `/team/${params.team_slug}/~/usage/account-abstraction`,
|
||||
label: "Account Abstraction",
|
||||
name: "Account Abstraction",
|
||||
path: `${usagePath}/account-abstraction`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
scrollableClassName="container max-w-7xl"
|
||||
/>
|
||||
|
||||
<div className="flex grow flex-col container max-w-7xl pt-6 pb-10">
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,148 +81,154 @@ export default async function RPCUsage(props: {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-8 py-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="font-bold text-3xl tracking-tight">
|
||||
RPC Usage Dashboard
|
||||
</h2>
|
||||
<div>
|
||||
<div className="flex flex-col gap-0.5 mb-4">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">RPC Usage</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Monitor your RPC usage and rate limits for the last 24 hours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">
|
||||
Plan Rate Limit
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-2xl capitalize">
|
||||
{currentRateLimit.toLocaleString()} RPS
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">
|
||||
Plan Rate Limit
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-2xl capitalize">
|
||||
{currentRateLimit.toLocaleString()} RPS
|
||||
</div>
|
||||
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
|
||||
</div>
|
||||
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">Peak Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-2xl">
|
||||
{Number(peakRate.peakRPS)} RPS
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">Peak Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-2xl">
|
||||
{Number(peakRate.peakRPS)} RPS
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${getStatusColor(peakPercentage)}`}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{peakPercentage > 100
|
||||
? `${(peakPercentage - 100).toFixed(0)}% over limit`
|
||||
: `${peakPercentage.toFixed(0)}% of limit`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${getStatusColor(peakPercentage)}`}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{peakPercentage > 100
|
||||
? `${(peakPercentage - 100).toFixed(0)}% over limit`
|
||||
: `${peakPercentage.toFixed(0)}% of limit`}
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
<ClockIcon className="mr-1 inline h-3 w-3" />
|
||||
{peakRateDate ? peakRateDate : "No Requests in last 24 hours"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">
|
||||
Total Requests
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-bold text-2xl">
|
||||
{totalRequests.toLocaleString()}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center text-muted-foreground text-xs">
|
||||
<CheckCircleIcon className="mr-1 h-3 w-3 text-green-500" />
|
||||
<span>
|
||||
{/* we count both included and overage as included */}
|
||||
{totalRequests === 0
|
||||
? "0.0"
|
||||
: (
|
||||
((Number(totalCounts.includedCount) +
|
||||
Number(totalCounts.overageCount)) /
|
||||
Number(totalRequests)) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
% successful requests
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
<ClockIcon className="mr-1 inline h-3 w-3" />
|
||||
{peakRateDate ? peakRateDate : "No Requests in last 24 hours"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">
|
||||
Total Requests
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-bold text-2xl">
|
||||
{totalRequests.toLocaleString()}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center text-muted-foreground text-xs">
|
||||
<CheckCircleIcon className="mr-1 h-3 w-3 text-green-500" />
|
||||
<span>
|
||||
{/* we count both included and overage as included */}
|
||||
{totalRequests === 0
|
||||
? "0.0"
|
||||
: (
|
||||
((Number(totalCounts.includedCount) +
|
||||
Number(totalCounts.overageCount)) /
|
||||
Number(totalRequests)) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
% successful requests
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">
|
||||
Rate Limited
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-bold text-2xl">
|
||||
{totalCounts.rateLimitedCount.toLocaleString()}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center text-muted-foreground text-xs">
|
||||
<XCircleIcon className="mr-1 h-3 w-3 text-red-500" />
|
||||
<span>
|
||||
{/* if there are no requests, we show 100% success rate */}
|
||||
{totalRequests === 0
|
||||
? "0.0"
|
||||
: (
|
||||
(Number(totalCounts.rateLimitedCount) /
|
||||
Number(totalRequests)) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
% of requests
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="font-medium text-sm">Rate Limited</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-bold text-2xl">
|
||||
{totalCounts.rateLimitedCount.toLocaleString()}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center text-muted-foreground text-xs">
|
||||
<XCircleIcon className="mr-1 h-3 w-3 text-red-500" />
|
||||
<span>
|
||||
{/* if there are no requests, we show 100% success rate */}
|
||||
{totalRequests === 0
|
||||
? "0.0"
|
||||
: (
|
||||
(Number(totalCounts.rateLimitedCount) /
|
||||
Number(totalRequests)) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
% of requests
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{peakPercentage > 100 && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Rate Limit Exceeded</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your peak usage of {Number(peakRate.peakRPS)} RPS has exceeded
|
||||
your plan limit of {currentRateLimit} RPS. Consider upgrading your
|
||||
plan to avoid rate limiting.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{peakPercentage > 80 && peakPercentage <= 100 && (
|
||||
<Alert
|
||||
className="border-yellow-500 text-yellow-700"
|
||||
variant="warning"
|
||||
>
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Approaching Rate Limit</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your peak usage is at {peakPercentage.toFixed(0)}% of your plan
|
||||
limit. You may experience rate limiting during high traffic
|
||||
periods.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<RateGraph
|
||||
currentRateLimit={currentRateLimit}
|
||||
data={averageRate}
|
||||
peakPercentage={peakPercentage}
|
||||
/>
|
||||
|
||||
<CountGraph
|
||||
currentRateLimit={currentRateLimit}
|
||||
data={averageRate}
|
||||
peakPercentage={peakPercentage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{peakPercentage > 100 && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Rate Limit Exceeded</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your peak usage of {Number(peakRate.peakRPS)} RPS has exceeded your
|
||||
plan limit of {currentRateLimit} RPS. Consider upgrading your plan
|
||||
to avoid rate limiting.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{peakPercentage > 80 && peakPercentage <= 100 && (
|
||||
<Alert className="border-yellow-500 text-yellow-700" variant="warning">
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Approaching Rate Limit</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your peak usage is at {peakPercentage.toFixed(0)}% of your plan
|
||||
limit. You may experience rate limiting during high traffic periods.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<RateGraph
|
||||
currentRateLimit={currentRateLimit}
|
||||
data={averageRate}
|
||||
peakPercentage={peakPercentage}
|
||||
/>
|
||||
|
||||
<CountGraph
|
||||
currentRateLimit={currentRateLimit}
|
||||
data={averageRate}
|
||||
peakPercentage={peakPercentage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,11 +151,14 @@ export const YourFilesSection = (props: { authToken: string }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg tracking-tight">
|
||||
Your Pinned Files
|
||||
</h2>
|
||||
<div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">Your files</h2>
|
||||
<p className="text-muted-foreground">
|
||||
These files are stored and pinned on the IPFS
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-2" />
|
||||
<div className="h-4" />
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export const SmartWalletsBillingAlert = (props: { teamSlug: string }) => {
|
||||
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
|
||||
<AlertDescription>
|
||||
To enable AA on mainnet chains,{" "}
|
||||
<UnderlineLink href={`/team/${props.teamSlug}/~/settings/billing`}>
|
||||
<UnderlineLink href={`/team/${props.teamSlug}/~/billing`}>
|
||||
subscribe to a billing plan.
|
||||
</UnderlineLink>
|
||||
</AlertDescription>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
SettingsIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
|
||||
import { getProject, getProjects, type Project } from "@/api/projects";
|
||||
import { getTeamBySlug, getTeams } from "@/api/team";
|
||||
import { CustomChatButton } from "@/components/chat/CustomChatButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { AnnouncementBanner } from "../../../../../../@/components/misc/AnnouncementBanner";
|
||||
import { siwaExamplePrompts } from "../../../../(dashboard)/support/definitions";
|
||||
import { getValidAccount } from "../../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedIn } from "../../../components/TeamHeader/team-header-logged-in.client";
|
||||
import { StaffModeNotice } from "../../(team)/_components/StaffModeNotice";
|
||||
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
|
||||
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
|
||||
import { getEngineInstances } from "./engine/dedicated/_utils/getEngineInstances";
|
||||
@@ -62,24 +61,12 @@ export default async function ProjectLayout(props: {
|
||||
project,
|
||||
});
|
||||
|
||||
const isStaffMode = !teams.some((t) => t.slug === team.slug);
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex h-dvh min-w-0 grow flex-col">
|
||||
{!teams.some((t) => t.slug === team.slug) && (
|
||||
<div className="bg-warning-text">
|
||||
<div className="container flex items-center justify-between py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-bold text-white text-xl">👀 STAFF MODE 👀</p>
|
||||
<p className="text-sm text-white">
|
||||
You can only view this team, not take any actions.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="default">
|
||||
<Link href="/team/~">Leave Staff Mode</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isStaffMode && <StaffModeNotice />}
|
||||
<div className="sticky top-0 z-10 border-border border-b bg-card">
|
||||
<AnnouncementBanner />
|
||||
<TeamHeaderLoggedIn
|
||||
|
||||
@@ -19,12 +19,14 @@ import {
|
||||
getWalletUsers,
|
||||
isProjectActive,
|
||||
} from "@/api/analytics";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject, type Project } from "@/api/projects";
|
||||
import {
|
||||
type DurationId,
|
||||
getLastNDaysRange,
|
||||
type Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
|
||||
import { RangeSelector } from "@/components/analytics/range-selector";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
@@ -35,8 +37,6 @@ import type {
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
} from "@/types/analytics";
|
||||
import { getAuthToken } from "../../../../../../@/api/auth-token";
|
||||
import { LoadingChartState } from "../../../../../../@/components/analytics/empty-chart-state";
|
||||
import { loginRedirect } from "../../../../login/loginRedirect";
|
||||
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
|
||||
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
|
||||
@@ -623,7 +623,11 @@ export function Header(props: {
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
|
||||
{showRangeSelector && (
|
||||
<div className="max-sm:w-full">
|
||||
<RangeSelector interval={interval} range={range} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-1
@@ -34,7 +34,14 @@ type ExecutionResult4337Serialized =
|
||||
}
|
||||
| {
|
||||
status: "FAILED";
|
||||
error: string;
|
||||
error: {
|
||||
stage: string;
|
||||
message: string;
|
||||
errorCode: string;
|
||||
inner_error: object;
|
||||
nonce_used: string;
|
||||
account_address: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
status: "SUBMITTED";
|
||||
|
||||
+20
-7
@@ -3,7 +3,7 @@
|
||||
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type ThirdwebClient, toEther } from "thirdweb";
|
||||
import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb";
|
||||
import type { Project } from "@/api/projects";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -43,10 +43,14 @@ export function TransactionDetailsUI({
|
||||
const status = executionResult?.status as keyof typeof statusDetails;
|
||||
const errorMessage =
|
||||
executionResult && "error" in executionResult
|
||||
? executionResult.error
|
||||
? executionResult.error.message
|
||||
: executionResult && "revertData" in executionResult
|
||||
? executionResult.revertData?.revertReason
|
||||
: null;
|
||||
const errorDetails =
|
||||
executionResult && "error" in executionResult
|
||||
? executionResult.error
|
||||
: undefined;
|
||||
|
||||
const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined;
|
||||
const explorer = chain?.explorers?.[0];
|
||||
@@ -64,7 +68,7 @@ export function TransactionDetailsUI({
|
||||
// Gas information
|
||||
const gasUsed =
|
||||
executionResult && "actualGasUsed" in executionResult
|
||||
? `${executionResult.actualGasUsed}`
|
||||
? `${isHex(executionResult.actualGasUsed) ? hexToNumber(executionResult.actualGasUsed) : executionResult.actualGasUsed}`
|
||||
: "N/A";
|
||||
|
||||
const gasCost =
|
||||
@@ -244,9 +248,16 @@ export function TransactionDetailsUI({
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{errorDetails ? (
|
||||
<CodeClient
|
||||
code={JSON.stringify(errorDetails, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -328,7 +339,9 @@ export function TransactionDetailsUI({
|
||||
Block Number
|
||||
</div>
|
||||
<div className="text-sm md:w-2/3">
|
||||
{transaction.confirmedAtBlockNumber}
|
||||
{isHex(transaction.confirmedAtBlockNumber)
|
||||
? hexToNumber(transaction.confirmedAtBlockNumber)
|
||||
: transaction.confirmedAtBlockNumber}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
type Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { RangeSelector } from "@/components/analytics/range-selector";
|
||||
import type { InAppWalletStats } from "../../../../../../../../../@/types/analytics";
|
||||
import type { InAppWalletStats } from "@/types/analytics";
|
||||
import { InAppWalletUsersChartCardUI } from "./InAppWalletUsersChartCard";
|
||||
|
||||
type InAppWalletAnalyticsProps = {
|
||||
|
||||
+3
@@ -29,6 +29,7 @@ function isAbiInput(
|
||||
}
|
||||
|
||||
// Helper to recursively get canonical type for ABI input (handles tuples)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
function getCanonicalType(input: any): string {
|
||||
if (input.type.startsWith("tuple")) {
|
||||
// Recursively build tuple type string
|
||||
@@ -93,6 +94,7 @@ export const extractEventSignatures = (
|
||||
const canonicalInputs = Array.isArray(event.inputs)
|
||||
? event.inputs
|
||||
.filter((input: unknown) => isAbiInput(input))
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
.map((input: any) => getCanonicalType(input))
|
||||
.join(",")
|
||||
: "";
|
||||
@@ -175,6 +177,7 @@ export const extractFunctionSignatures = (
|
||||
const canonicalInputs = Array.isArray(func.inputs)
|
||||
? func.inputs
|
||||
.filter((input: unknown) => isAbiInput(input))
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
.map((input: any) => getCanonicalType(input))
|
||||
.join(",")
|
||||
: "";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getTeamBySlug, hasToCompleteTeamOnboarding } from "@/api/team";
|
||||
import { EnsureValidConnectedWalletLoginServer } from "@/components/misc/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
|
||||
import { getAuthToken } from "../../../../@/api/auth-token";
|
||||
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
|
||||
import {
|
||||
PastDueBanner,
|
||||
|
||||
@@ -10,13 +10,17 @@ export function AnalyticsHeader(props: {
|
||||
const { title, interval, range, showRangeSelector } = props;
|
||||
|
||||
return (
|
||||
<div className="container flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
|
||||
<div className="container max-w-7xl flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
|
||||
{showRangeSelector && (
|
||||
<div className="max-sm:w-full">
|
||||
<RangeSelector interval={interval} range={range} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-3
@@ -102,9 +102,7 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
|
||||
setHoveredTeam={setHoveredTeam}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
upgradeTeamLink={
|
||||
currentTeam
|
||||
? `/team/${currentTeam.slug}/~/settings/billing`
|
||||
: undefined
|
||||
currentTeam ? `/team/${currentTeam.slug}/~/billing` : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
|
||||
"extends": "//"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@tanstack/react-query": "5.80.7",
|
||||
"@vercel/functions": "2.1.0",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
|
||||
"extends": "//"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ function Example() {
|
||||
<${componentName}
|
||||
client={client}
|
||||
chain={defineChain(${options.payOptions.buyTokenChain.id})}
|
||||
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${
|
||||
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${options.payOptions.paymentMethods && options.payOptions.paymentMethods.length > 0 ? `\n\t paymentMethods={${JSON.stringify(options.payOptions.paymentMethods)}}` : ""}${
|
||||
options.payOptions.widget === "transaction"
|
||||
? `\n\t transaction={claimTo({
|
||||
contract: nftContract,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Address, Chain } from "thirdweb";
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import type { ThemeOverrides } from "thirdweb/react";
|
||||
import type { Address } from "thirdweb/utils";
|
||||
|
||||
export type BridgeComponentsPlaygroundOptions = {
|
||||
theme: {
|
||||
@@ -22,5 +23,7 @@ export type BridgeComponentsPlaygroundOptions = {
|
||||
|
||||
// transaction mode options
|
||||
transactionData?: string; // Simplified for demo; could be more complex in real implementation
|
||||
|
||||
paymentMethods: ("crypto" | "card")[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useId, useState } from "react";
|
||||
import type { Address } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
@@ -53,6 +54,8 @@ export function LeftSection(props: {
|
||||
const modalTitleIconId = useId();
|
||||
const modalDescriptionId = useId();
|
||||
const themeId = useId();
|
||||
const cryptoPaymentId = useId();
|
||||
const cardPaymentId = useId();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -161,6 +164,65 @@ export function LeftSection(props: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Methods */}
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<Label>Payment Methods</Label>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={payOptions.paymentMethods.includes(
|
||||
"crypto",
|
||||
)}
|
||||
id={cryptoPaymentId}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payOptions: {
|
||||
...v.payOptions,
|
||||
paymentMethods: checked
|
||||
? [
|
||||
...v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "crypto",
|
||||
),
|
||||
"crypto",
|
||||
]
|
||||
: v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "crypto",
|
||||
),
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={payOptions.paymentMethods.includes("card")}
|
||||
id={cardPaymentId}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payOptions: {
|
||||
...v.payOptions,
|
||||
paymentMethods: checked
|
||||
? [
|
||||
...v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "card",
|
||||
),
|
||||
"card",
|
||||
]
|
||||
: v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "card",
|
||||
),
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={cardPaymentId}>Card</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,6 +311,65 @@ export function LeftSection(props: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Methods */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Payment Methods</Label>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={payOptions.paymentMethods.includes(
|
||||
"crypto",
|
||||
)}
|
||||
id={cryptoPaymentId}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payOptions: {
|
||||
...v.payOptions,
|
||||
paymentMethods: checked
|
||||
? [
|
||||
...v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "crypto",
|
||||
),
|
||||
"crypto",
|
||||
]
|
||||
: v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "crypto",
|
||||
),
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={payOptions.paymentMethods.includes("card")}
|
||||
id={cardPaymentId}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payOptions: {
|
||||
...v.payOptions,
|
||||
paymentMethods: checked
|
||||
? [
|
||||
...v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "card",
|
||||
),
|
||||
"card",
|
||||
]
|
||||
: v.payOptions.paymentMethods.filter(
|
||||
(m) => m !== "card",
|
||||
),
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={cardPaymentId}>Card</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@ export function RightSection(props: {
|
||||
client={THIRDWEB_CLIENT}
|
||||
description={props.options.payOptions.description}
|
||||
image={props.options.payOptions.image}
|
||||
paymentMethods={props.options.payOptions.paymentMethods}
|
||||
theme={themeObj}
|
||||
title={props.options.payOptions.title}
|
||||
tokenAddress={props.options.payOptions.buyTokenAddress}
|
||||
@@ -81,6 +82,7 @@ export function RightSection(props: {
|
||||
getDefaultImage(props.options.theme.type)
|
||||
}
|
||||
name={props.options.payOptions.title || "Your Product Name"}
|
||||
paymentMethods={props.options.payOptions.paymentMethods}
|
||||
presetOptions={[1, 2, 3]}
|
||||
seller={props.options.payOptions.sellerAddress}
|
||||
theme={themeObj}
|
||||
@@ -95,6 +97,7 @@ export function RightSection(props: {
|
||||
client={THIRDWEB_CLIENT}
|
||||
description={props.options.payOptions.description}
|
||||
image={props.options.payOptions.image}
|
||||
paymentMethods={props.options.payOptions.paymentMethods}
|
||||
theme={themeObj}
|
||||
title={props.options.payOptions.title}
|
||||
transaction={claimTo({
|
||||
|
||||
@@ -13,6 +13,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = {
|
||||
buyTokenChain: arbitrum,
|
||||
description: "",
|
||||
image: "",
|
||||
paymentMethods: ["crypto", "card"],
|
||||
sellerAddress: "0x0000000000000000000000000000000000000000",
|
||||
title: "",
|
||||
transactionData: "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
|
||||
"extends": "//"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"src/pages/**/*.{js,jsx,ts,tsx}",
|
||||
"scripts/*.ts"
|
||||
],
|
||||
"ignore": ["src/components/ui/**"],
|
||||
"ignore": ["src/components/ui/**", "src/icons/**"],
|
||||
"ignoreBinaries": ["only-allow"],
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/chains",
|
||||
|
||||
@@ -33,15 +33,15 @@ import { ThirdwebIcon } from "../icons/thirdweb";
|
||||
const links = [
|
||||
{
|
||||
href: "/connect",
|
||||
name: "Connect",
|
||||
name: "Wallets",
|
||||
},
|
||||
{
|
||||
href: "/pay",
|
||||
name: "Bridge",
|
||||
name: "Payments",
|
||||
},
|
||||
{
|
||||
href: "/engine",
|
||||
name: "Engine",
|
||||
name: "Transactions",
|
||||
},
|
||||
{
|
||||
href: "/contracts",
|
||||
@@ -181,7 +181,7 @@ const sdkLinks = [
|
||||
const supportLinks = [
|
||||
{
|
||||
href: "/knowledge-base",
|
||||
name: "Get thirdweb support",
|
||||
name: "Articles",
|
||||
},
|
||||
{
|
||||
href: "/account",
|
||||
@@ -216,6 +216,20 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden xl:block">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="mx-3 size-6" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
@@ -231,20 +245,6 @@ export function Header() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="mx-3 size-6" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
|
||||
sideBar={sidebar}
|
||||
sidebarHeader={
|
||||
<div className="flex-col items-center gap-1">
|
||||
<p className="py-5 font-semibold text-foreground text-lg">Connect</p>
|
||||
<p className="py-5 font-semibold text-foreground text-lg">Wallets</p>
|
||||
<PlatformSelector selected="Overview" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ export const metadata = createMetadata({
|
||||
},
|
||||
});
|
||||
|
||||
# What is Connect?
|
||||
# Wallets
|
||||
|
||||
Connect is the complete toolkit for connecting every user to your application. It features customizable onboarding flows, self-custodial in-app wallets, account abstraction, onramps, and a performant API to interact with the blockchain.
|
||||
Wallets is the complete toolkit for connecting every user to your application. It features customizable onboarding flows, self-custodial in-app wallets, account abstraction, onramps, and a performant API to interact with the blockchain.
|
||||
|
||||
## Live Playground
|
||||
|
||||
@@ -55,7 +55,7 @@ You can interact with the React SDK in the [Live Playground](https://playground.
|
||||
|
||||
## Quick starts
|
||||
|
||||
Get started with Connect in your preferred language.
|
||||
Get started with Wallets in your preferred language.
|
||||
|
||||
<Grid>
|
||||
<SDKCard
|
||||
@@ -78,7 +78,7 @@ Get started with Connect in your preferred language.
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
## With Connect, you can
|
||||
## With Wallets, you can
|
||||
|
||||
- **Connect to 500+ different wallet providers** with support for every EVM network
|
||||
- **Log in and authenticate your users** with customizable and secure [email, phone, passkeys and social login](https://portal.thirdweb.com/connect/in-app-wallet/overview) flows.
|
||||
@@ -90,7 +90,7 @@ Get started with Connect in your preferred language.
|
||||
<Callout variant="info" title="Have you generated a client ID?">
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
<p>
|
||||
You'll need a client ID to access Connect's free blockchain APIs, storage, and more.
|
||||
You'll need a client ID to access the Wallet APIs, storage, and more.
|
||||
</p>
|
||||
|
||||
<Button asChild variant='outline'>
|
||||
@@ -102,7 +102,7 @@ Get started with Connect in your preferred language.
|
||||
|
||||
## Supported Chains
|
||||
|
||||
Connect is supported on every EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist).
|
||||
Wallets are supported on every EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist).
|
||||
|
||||
<DocImage src={SupportedChains} />
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ export const sidebar: SideBar = {
|
||||
name: "FAQ",
|
||||
},
|
||||
],
|
||||
name: "Onboard Users",
|
||||
name: "User Wallets",
|
||||
},
|
||||
{ separator: true },
|
||||
// User identity
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user