Compare commits
80
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe9753be6 | ||
|
|
eeb993df75 | ||
|
|
367a270bba | ||
|
|
e91581aec6 | ||
|
|
985c4b0804 | ||
|
|
9fb5401a93 | ||
|
|
e54867550e | ||
|
|
514647cb53 | ||
|
|
eb8638e0c2 | ||
|
|
a6f85ac949 | ||
|
|
726bbde9ca | ||
|
|
025c581be6 | ||
|
|
522968a505 | ||
|
|
454f063f98 | ||
|
|
78cbc412ce | ||
|
|
79c50cb311 | ||
|
|
764ee8ca47 | ||
|
|
783d84467d | ||
|
|
687a733d4a | ||
|
|
96daa2654c | ||
|
|
a2ce5f56ab | ||
|
|
6beec7a35e | ||
|
|
31f371d065 | ||
|
|
fd8674a8a4 | ||
|
|
d3b0dc6646 | ||
|
|
98c5fd0b79 | ||
|
|
2f11259ccd | ||
|
|
97c09003dc | ||
|
|
5d0505c43c | ||
|
|
e552fda9e5 | ||
|
|
cddeda4280 | ||
|
|
cb72f67c92 | ||
|
|
222964dcbb | ||
|
|
ec37c74199 | ||
|
|
2ac1e86b8f | ||
|
|
bdc5964054 | ||
|
|
5fc07f7111 | ||
|
|
80729e9748 | ||
|
|
8832f87f4a | ||
|
|
39a43903ad | ||
|
|
6396729839 | ||
|
|
30fd225538 | ||
|
|
931bbd4952 | ||
|
|
3202ce105c | ||
|
|
36aa3f0937 | ||
|
|
ab43d9426a | ||
|
|
29938abfe3 | ||
|
|
5bb5a437cd | ||
|
|
8b681729a1 | ||
|
|
6c26012a71 | ||
|
|
90b5495efb | ||
|
|
71575afef3 | ||
|
|
70adb57293 | ||
|
|
96fa4102ba | ||
|
|
fc56142880 | ||
|
|
842baa0513 | ||
|
|
601958d5b7 | ||
|
|
bec828de86 | ||
|
|
a576a5b858 | ||
|
|
5e3be734a3 | ||
|
|
ce6d873ccc | ||
|
|
bbe75b0bc8 | ||
|
|
cefa4eb097 | ||
|
|
63ceb2874c | ||
|
|
509c7981ee | ||
|
|
8ea9420cc4 | ||
|
|
8dda1139dc | ||
|
|
a637cab051 | ||
|
|
c31f919cd4 | ||
|
|
ffcd199381 | ||
|
|
b1d78b1f42 | ||
|
|
bc348ea953 | ||
|
|
43a24e2ed4 | ||
|
|
7b40f2e8e4 | ||
|
|
c1d94aec4f | ||
|
|
ebd5908d4a | ||
|
|
21b032d1b8 | ||
|
|
d0616c4486 | ||
|
|
88db5eaeab | ||
|
|
09b1c28c09 |
+2
-1
@@ -27,4 +27,5 @@ sitemap*.xml
|
||||
packages/*/typedoc/*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
storybook-static
|
||||
.aider*
|
||||
|
||||
@@ -60,6 +60,8 @@ module.exports = {
|
||||
"AspectRatio",
|
||||
"useToast",
|
||||
"useClipboard",
|
||||
"Badge",
|
||||
"Stack",
|
||||
// also the types
|
||||
"ButtonProps",
|
||||
"BadgeProps",
|
||||
@@ -71,6 +73,8 @@ module.exports = {
|
||||
"MenuGroupProps",
|
||||
"MenuItemProps",
|
||||
"AspectRatioProps",
|
||||
"BadgeProps",
|
||||
"StackProps",
|
||||
],
|
||||
message:
|
||||
'Use the equivalent component from "tw-components" instead.',
|
||||
|
||||
@@ -5,10 +5,10 @@ const ContentSecurityPolicy = `
|
||||
img-src * data: blob:;
|
||||
media-src * data: blob:;
|
||||
object-src 'none';
|
||||
style-src 'self' 'unsafe-inline' https://vercel.live;
|
||||
font-src 'self' https://vercel.live https://assets.vercel.com;
|
||||
style-src 'self' 'unsafe-inline' vercel.live;
|
||||
font-src 'self' vercel.live assets.vercel.com framerusercontent.com;
|
||||
frame-src * data:;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com;
|
||||
connect-src * data: blob:;
|
||||
worker-src 'self' blob:;
|
||||
block-all-mixed-content;
|
||||
@@ -39,6 +39,9 @@ const securityHeaders = [
|
||||
|
||||
const redirects = require("./redirects");
|
||||
|
||||
// add framer paths here
|
||||
const FRAMER_PATHS = ["/connect/sign-in", "/contracts/modular-contracts"];
|
||||
|
||||
/**
|
||||
* @returns {import('next').RemotePattern[]}
|
||||
*/
|
||||
@@ -128,6 +131,10 @@ const moduleExports = {
|
||||
source: "/thirdweb.eth/:path*",
|
||||
destination: "/deployer.thirdweb.eth/:path*",
|
||||
},
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
source: path,
|
||||
destination: `https://landing.thirdweb.com${path}`,
|
||||
})),
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
||||
+17
-18
@@ -45,8 +45,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "1.1.2",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@sentry/nextjs": "8.30.0",
|
||||
"@sentry/nextjs": "8.32.0",
|
||||
"@shazow/whatsabi": "^0.14.1",
|
||||
"@stripe/react-stripe-js": "^2.7.3",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
@@ -62,8 +61,9 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.5.6",
|
||||
"framer-motion": "11.9.0",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
@@ -88,7 +88,6 @@
|
||||
"react-icons": "^5.2.1",
|
||||
"react-intersection-observer": "^9.10.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "^2.12.7",
|
||||
@@ -110,21 +109,21 @@
|
||||
"@next/bundle-analyzer": "14.2.13",
|
||||
"@next/eslint-plugin-next": "14.2.13",
|
||||
"@playwright/test": "1.47.2",
|
||||
"@storybook/addon-essentials": "8.3.2",
|
||||
"@storybook/addon-interactions": "8.3.2",
|
||||
"@storybook/addon-links": "8.3.2",
|
||||
"@storybook/addon-onboarding": "8.3.2",
|
||||
"@storybook/addon-viewport": "8.3.2",
|
||||
"@storybook/blocks": "8.3.2",
|
||||
"@storybook/nextjs": "8.3.2",
|
||||
"@storybook/react": "8.3.2",
|
||||
"@storybook/test": "8.3.2",
|
||||
"@storybook/addon-essentials": "8.3.4",
|
||||
"@storybook/addon-interactions": "8.3.4",
|
||||
"@storybook/addon-links": "8.3.4",
|
||||
"@storybook/addon-onboarding": "8.3.4",
|
||||
"@storybook/addon-viewport": "8.3.4",
|
||||
"@storybook/blocks": "8.3.4",
|
||||
"@storybook/nextjs": "8.3.4",
|
||||
"@storybook/react": "8.3.4",
|
||||
"@storybook/test": "8.3.4",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.8",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
@@ -136,12 +135,12 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.8.4",
|
||||
"eslint-plugin-react-compiler": "0.0.0-experimental-ca16900-20240916",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"knip": "^5.30.5",
|
||||
"eslint-plugin-storybook": "^0.9.0",
|
||||
"knip": "^5.30.6",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.3.2",
|
||||
"tailwindcss": "3.4.12",
|
||||
"storybook": "8.3.4",
|
||||
"tailwindcss": "3.4.13",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -34,7 +32,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -59,7 +57,7 @@ export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
const TeamAccountRole = {
|
||||
OWNER: "OWNER",
|
||||
MEMBER: "MEMBER",
|
||||
@@ -39,7 +37,7 @@ export async function getMembers(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/members`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type Team = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -35,7 +33,7 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams/${slug}`, {
|
||||
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
@@ -57,7 +55,7 @@ export async function getTeams() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams`, {
|
||||
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { GradientAvatar } from "./GradientAvatar";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar id={undefined} src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Src, id=foo - Skeleton">
|
||||
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
|
||||
<GradientAvatar id={"foo"} src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
|
||||
<GradientAvatar id={"bar"} src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
|
||||
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTest() {
|
||||
const [data, setData] = useState<undefined | { src: string; id: string }>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
} else {
|
||||
setData({
|
||||
id: "foo",
|
||||
src: "https://picsum.photos/400",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
<p> Src+ID is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<GradientAvatar
|
||||
className="size-6"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
const gradients = [
|
||||
["#fca5a5", "#b91c1c"],
|
||||
["#fdba74", "#c2410c"],
|
||||
["#fcd34d", "#b45309"],
|
||||
["#fde047", "#a16207"],
|
||||
["#a3e635", "#4d7c0f"],
|
||||
["#86efac", "#15803d"],
|
||||
["#67e8f9", "#0e7490"],
|
||||
["#7dd3fc", "#0369a1"],
|
||||
["#93c5fd", "#1d4ed8"],
|
||||
["#a5b4fc", "#4338ca"],
|
||||
["#c4b5fd", "#6d28d9"],
|
||||
["#d8b4fe", "#7e22ce"],
|
||||
["#f0abfc", "#a21caf"],
|
||||
["#f9a8d4", "#be185d"],
|
||||
["#fda4af", "#be123c"],
|
||||
];
|
||||
|
||||
function getGradientForString(str: string) {
|
||||
const number = Math.abs(
|
||||
str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
|
||||
);
|
||||
const index = number % gradients.length;
|
||||
return gradients[index];
|
||||
}
|
||||
|
||||
export function GradientAvatar(props: {
|
||||
src: string | undefined;
|
||||
id: string | undefined;
|
||||
className: string;
|
||||
}) {
|
||||
const gradient = useMemo(() => {
|
||||
if (!props.id) {
|
||||
return undefined;
|
||||
}
|
||||
return getGradientForString(props.id);
|
||||
}, [props.id]);
|
||||
|
||||
return (
|
||||
<Img
|
||||
src={props.src}
|
||||
className={cn("rounded-full", props.className)}
|
||||
fallback={
|
||||
gradient ? (
|
||||
<div
|
||||
style={{
|
||||
background: `linear-gradient(45deg, ${gradient[0]} 0%, ${gradient[1]} 100%)`,
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProjectAvatar } from "./ProjectAvatar";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Avatars/ProjectAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
<ProjectAvatar src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
|
||||
<ProjectAvatar src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTest() {
|
||||
const [data, setData] = useState<undefined | { src: string; name: string }>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
} else {
|
||||
setData({
|
||||
name: "foo",
|
||||
src: "https://picsum.photos/400",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
<p> Src+Name is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<ProjectAvatar src={data?.src} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<ProjectAvatar
|
||||
src={data ? "invalid-src" : undefined}
|
||||
className="size-6"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { BoxIcon } from "lucide-react";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function ProjectAvatar(props: {
|
||||
src: string | undefined;
|
||||
className: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<Img
|
||||
src={props.src}
|
||||
className={cn("rounded-lg border", props.className)}
|
||||
alt={""}
|
||||
fallback={
|
||||
<div className="flex items-center justify-center bg-muted/50">
|
||||
<BoxIcon className="size-[50%] text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,25 +4,45 @@ import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import type { SidebarLink } from "./Sidebar";
|
||||
import {
|
||||
RenderSidebarLinks,
|
||||
type SidebarBaseLink,
|
||||
type SidebarLink,
|
||||
} from "./Sidebar";
|
||||
|
||||
export function MobileSidebar(props: {
|
||||
links?: SidebarLink[];
|
||||
links: SidebarLink[];
|
||||
footer?: React.ReactNode;
|
||||
trigger?: React.ReactNode;
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const activeLink = props.links?.find((link) => {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
});
|
||||
|
||||
for (const link of props.links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.links, pathname]);
|
||||
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
@@ -41,7 +61,7 @@ export function MobileSidebar(props: {
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{props.trigger || defaultTrigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="rounded-t-xl rounded-b-none p-4"
|
||||
className="no-scrollbar max-h-[80vh] overflow-auto rounded-t-xl rounded-b-none p-4"
|
||||
dialogCloseClassName="hidden"
|
||||
onClick={(e) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
@@ -49,23 +69,7 @@ export function MobileSidebar(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.links?.map((link) => {
|
||||
return (
|
||||
<Button size="sm" variant="ghost" asChild key={link.href}>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="!text-left flex h-auto justify-start gap-2 py-3"
|
||||
activeClassName="bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
tracking={link.tracking}
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
{props.footer}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
|
||||
export type SidebarLink = {
|
||||
export type SidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
@@ -14,6 +14,13 @@ export type SidebarLink = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SidebarLink =
|
||||
| SidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: SidebarBaseLink[];
|
||||
};
|
||||
|
||||
type SidebarContentProps = {
|
||||
header?: React.ReactNode;
|
||||
links: SidebarLink[];
|
||||
@@ -31,23 +38,42 @@ export function Sidebar(props: SidebarContentProps) {
|
||||
<div className="pt-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.links?.map((link) => {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
|
||||
activeClassName="text-foreground"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.links.map((link, i) => {
|
||||
if ("group" in link) {
|
||||
return (
|
||||
<div className={cn({ "mt-6": i !== 0 })}>
|
||||
<p className={cn("px-3 py-2 font-medium text-foreground")}>
|
||||
{link.group}
|
||||
</p>
|
||||
<RenderSidebarLinks links={link.links} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
|
||||
activeClassName="text-foreground"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
|
||||
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
|
||||
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
|
||||
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
|
||||
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
|
||||
import { RedditIcon } from "components/icons/brand-icons/RedditIcon";
|
||||
import { TiktokIcon } from "components/icons/brand-icons/TiktokIcon";
|
||||
import { XIcon } from "components/icons/brand-icons/XIcon";
|
||||
import { YoutubeIcon } from "components/icons/brand-icons/YoutubeIcon";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
SiDiscord,
|
||||
SiGithub,
|
||||
SiInstagram,
|
||||
SiLinkedin,
|
||||
SiReddit,
|
||||
SiTiktok,
|
||||
SiX,
|
||||
SiYoutube,
|
||||
} from "react-icons/si";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
@@ -35,17 +33,17 @@ export function AppFooter(props: AppFooterProps) {
|
||||
<div className="flex flex-row gap-3">
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://github.com/thirdweb-dev" target="_blank">
|
||||
<SiGithub className="size-5 text-muted-foreground" />
|
||||
<GithubIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.tiktok.com/@thirdweb" target="_blank">
|
||||
<SiTiktok className="size-5 text-muted-foreground" />
|
||||
<TiktokIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.instagram.com/thirdweb/" target="_blank">
|
||||
<SiInstagram className="size-5 text-muted-foreground" />
|
||||
<InstagramIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
@@ -53,27 +51,27 @@ export function AppFooter(props: AppFooterProps) {
|
||||
href="hhttps://www.linkedin.com/company/third-web/"
|
||||
target="_blank"
|
||||
>
|
||||
<SiLinkedin className="size-5 text-muted-foreground" />
|
||||
<LinkedInIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.youtube.com/@thirdweb_" target="_blank">
|
||||
<SiYoutube className="size-5 text-muted-foreground" />
|
||||
<YoutubeIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://discord.gg/thirdweb" target="_blank">
|
||||
<SiDiscord className="size-5 text-muted-foreground" />
|
||||
<DiscordIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.reddit.com/r/thirdweb/" target="_blank">
|
||||
<SiReddit className="size-5 text-muted-foreground" />
|
||||
<RedditIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://x.com/thirdweb" target="_blank">
|
||||
<SiX className="size-5 text-muted-foreground" />
|
||||
<XIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -18,11 +9,9 @@ import {
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -37,73 +26,65 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<div className="rounded-lg border border-border bg-muted/50 px-4 pt-10 pb-4">
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
<linearGradient
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function InlineCode({
|
||||
code,
|
||||
className,
|
||||
}: { code: string; className?: string }) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
"inline-block rounded bg-muted px-2 font-mono text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { Dot } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = "InputOTP";
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = "InputOTPGroup";
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-input border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = "InputOTPSlot";
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -165,6 +165,8 @@ function useUnderline<El extends HTMLElement>() {
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
} else if (lineRef.current) {
|
||||
lineRef.current.style.width = "0px";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,3 +20,6 @@ export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
export const DASHBOARD_STORAGE_URL =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
|
||||
"https://storage.thirdweb.com";
|
||||
|
||||
export const API_SERVER_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export type Store<T> = {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# DO NOT ADD NEW THINGS INSIDE THIS FOLDER OR SUB_FOLDERS
|
||||
|
||||
This part of the dashboard is deprecated, and we are actively working on removing it. Please do not add new things here. If you need to add new things, please add them to the appropriate folder in the src folder.
|
||||
@@ -1,9 +1,3 @@
|
||||
export const networkKeys = {
|
||||
all: ["network"] as const,
|
||||
chain: (chainId?: number) => [...networkKeys.all, chainId] as const,
|
||||
multiChainRegistry: ["multi-chain-registry"] as const,
|
||||
};
|
||||
|
||||
export const accountKeys = {
|
||||
all: ["account"] as const,
|
||||
wallet: (walletAddress: string) =>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { networkKeys, useDashboardEVMChainId } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
type MutationFunction,
|
||||
type QueryFunction,
|
||||
type QueryKey,
|
||||
type UseMutationOptions,
|
||||
type UseQueryOptions,
|
||||
type UseQueryResult,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function useQueryWithNetwork<
|
||||
TQueryFnData = unknown,
|
||||
TError = unknown,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
queryKey: TQueryKey,
|
||||
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>,
|
||||
): UseQueryResult<TData, TError> {
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
|
||||
const mergedOptions: Omit<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
> = {
|
||||
...options,
|
||||
enabled: !!(activeChainId && options?.enabled),
|
||||
};
|
||||
|
||||
const combinedQueryKey = (
|
||||
networkKeys.chain(activeChainId) as readonly unknown[]
|
||||
).concat(queryKey) as unknown as TQueryKey;
|
||||
|
||||
return useQuery({
|
||||
...mergedOptions,
|
||||
queryKey: combinedQueryKey,
|
||||
queryFn,
|
||||
});
|
||||
}
|
||||
export function useMutationWithInvalidate<
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
TVariables = void,
|
||||
TContext = unknown,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
mutationFn: MutationFunction<TData, TVariables>,
|
||||
options?: Omit<
|
||||
UseMutationOptions<TData, TError, TVariables, TContext>,
|
||||
"mutationFn" | "onSuccess"
|
||||
> & {
|
||||
onSuccess?: (
|
||||
data: TData,
|
||||
variables: TVariables,
|
||||
context: TContext | undefined,
|
||||
wrapCacheKeys: (
|
||||
cacheKeysToInvalidate: TQueryKey[],
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: FIX ME
|
||||
) => Promise<void[]>,
|
||||
) => Promise<unknown> | undefined;
|
||||
},
|
||||
) {
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidate = useCallback(
|
||||
(cacheKeysToInvalidate: TQueryKey[]) => {
|
||||
return Promise.all(
|
||||
cacheKeysToInvalidate.map((cacheKey) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: (
|
||||
networkKeys.chain(activeChainId) as readonly unknown[]
|
||||
).concat(cacheKey) as unknown[],
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
[queryClient, activeChainId],
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
...options,
|
||||
mutationFn,
|
||||
onSuccess: (...args) => {
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(...args, invalidate);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export type EVMContractInfo = {
|
||||
// using null instead of undefined here so that this type can be JSON stringified
|
||||
@@ -14,78 +6,3 @@ export type EVMContractInfo = {
|
||||
chainSlug: string;
|
||||
contractAddress: string;
|
||||
};
|
||||
|
||||
type SetEVMContractInfo = (info: EVMContractInfo) => void;
|
||||
|
||||
// Legacy: will be removed with contract page -> app router refactor
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const EVMContractInfoContext = createContext<EVMContractInfo | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Legacy: will be removed with contract page -> app router refactor
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const SetEVMContractInfoContext = createContext<SetEVMContractInfo | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// TODO let's move this out of here eventually
|
||||
export function EVMContractInfoProvider(props: {
|
||||
children: React.ReactNode;
|
||||
value?: EVMContractInfo;
|
||||
}) {
|
||||
const [value, setValue] = useState<EVMContractInfo | undefined>(props.value);
|
||||
|
||||
// keep the state in sync with the props.value
|
||||
// this is required when route changes and page goes from fallback to actual page
|
||||
|
||||
const _setValue = useCallback((info?: EVMContractInfo) => {
|
||||
setValue((prevValue) => {
|
||||
if (
|
||||
prevValue?.chainSlug === info?.chainSlug &&
|
||||
prevValue?.contractAddress === info?.contractAddress &&
|
||||
info?.chain === null
|
||||
) {
|
||||
return prevValue;
|
||||
}
|
||||
return info;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// no better way right now - this whole file is going away with RSC migration
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
_setValue(props.value);
|
||||
}, [_setValue, props.value]);
|
||||
// when we move from rendering the fallback page to the actual page, props.value is correct value but state lags behind with undefined value
|
||||
// in that case we use the props.value instead of providing undefined and update the state with effect afterwards
|
||||
// this only happens ONCE, when the page is not yet created on server. once it is created and cached. value will always be defined
|
||||
const providedValue = value || props.value;
|
||||
|
||||
return (
|
||||
<EVMContractInfoContext.Provider value={providedValue}>
|
||||
<SetEVMContractInfoContext.Provider value={_setValue}>
|
||||
{props.children}
|
||||
</SetEVMContractInfoContext.Provider>
|
||||
</EVMContractInfoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useEVMContractInfo() {
|
||||
return useContext(EVMContractInfoContext);
|
||||
}
|
||||
|
||||
export function useSetEVMContractInfo() {
|
||||
const setter = useContext(SetEVMContractInfoContext);
|
||||
invariant(
|
||||
setter,
|
||||
"useSetEVMContractInfo must be used inside EVMContractInfoProvider",
|
||||
);
|
||||
return setter;
|
||||
}
|
||||
|
||||
// for EVM - get network info from context
|
||||
export function useDashboardEVMChainId() {
|
||||
const contractInfo = useEVMContractInfo();
|
||||
return contractInfo?.chain?.chainId;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
|
||||
import invariant from "tiny-invariant";
|
||||
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
@@ -67,6 +66,7 @@ export type Account = {
|
||||
paymentFailureCode: string;
|
||||
serviceCutoffDate: string;
|
||||
}[];
|
||||
// TODO - add image URL
|
||||
};
|
||||
|
||||
interface UpdateAccountInput {
|
||||
@@ -478,8 +478,8 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: { plan: string; feedback?: string }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: { plan: string; feedback?: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
|
||||
@@ -505,27 +505,26 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
onSuccess: async () => {
|
||||
return Promise.all([
|
||||
// invalidate usage data as limits are different
|
||||
await queryClient.invalidateQueries({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
|
||||
return queryClient.invalidateQueries({
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
});
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateAccountNotificationsInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccountNotificationsInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, {
|
||||
@@ -544,14 +543,12 @@ export function useUpdateNotifications() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateBillingSession(enabled = false) {
|
||||
@@ -588,8 +585,8 @@ export function useConfirmEmail() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: ConfirmEmailInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ConfirmEmailInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, {
|
||||
@@ -608,31 +605,29 @@ export function useConfirmEmail() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useResendEmailConfirmation() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(
|
||||
@@ -654,22 +649,20 @@ export function useResendEmailConfirmation() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePaymentMethod() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (paymentMethodId: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (paymentMethodId: string) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/paymentMethod`, {
|
||||
@@ -690,14 +683,12 @@ export function useCreatePaymentMethod() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys() {
|
||||
@@ -760,8 +751,8 @@ export function useUpdateApiKey() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateKeyInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateKeyInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, {
|
||||
@@ -781,22 +772,20 @@ export function useUpdateApiKey() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useRevokeApiKey() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (id: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, {
|
||||
@@ -815,14 +804,12 @@ export function useRevokeApiKey() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const usePolicies = (serviceId?: string) => {
|
||||
@@ -851,8 +838,11 @@ export const usePolicies = (serviceId?: string) => {
|
||||
|
||||
export const useUpdatePolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutationWithInvalidate(
|
||||
async (input: { serviceId: string; data: ApiKeyServicePolicy }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: {
|
||||
serviceId: string;
|
||||
data: ApiKeyServicePolicy;
|
||||
}) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -870,22 +860,20 @@ export const useUpdatePolicies = () => {
|
||||
}
|
||||
return json.data as ApiKeyServicePolicy;
|
||||
},
|
||||
{
|
||||
onSuccess: (_, variables) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["policies", variables.serviceId],
|
||||
});
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["policies", variables.serviceId],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export function useRevokeAuthorizedWallet() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (variables: { authorizedWalletId: string }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { authorizedWalletId: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const { authorizedWalletId } = variables;
|
||||
@@ -909,16 +897,12 @@ export function useRevokeAuthorizedWallet() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: authorizedWallets.authorizedWallets(
|
||||
user?.address as string,
|
||||
),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: authorizedWallets.authorizedWallets(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useAuthorizedWallets() {
|
||||
|
||||
@@ -14,41 +14,52 @@ import { getContractMetadata, name, symbol } from "thirdweb/extensions/common";
|
||||
export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
|
||||
queryFn: async () => {
|
||||
const [contractMetadata, _name, _symbol, compilerMetadata] =
|
||||
await Promise.all([
|
||||
getContractMetadata({ contract }).catch(() => ({
|
||||
image: "",
|
||||
name: "",
|
||||
symbol: "",
|
||||
})),
|
||||
name({ contract }).catch(() => ""),
|
||||
symbol({ contract }).catch(() => ""),
|
||||
getCompilerMetadata(contract).catch(() => undefined),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
|
||||
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
|
||||
* In that case, we split the solidity name on ":" and take the second part
|
||||
*/
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
}
|
||||
}
|
||||
|
||||
const contractName =
|
||||
contractMetadata?.name || _name || compilerMetadata?.name || "";
|
||||
const contractSymbol = contractMetadata?.symbol || _symbol || "";
|
||||
|
||||
return {
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
image: contractMetadata.image || "",
|
||||
contractType: compilerMetadata?.name || "",
|
||||
};
|
||||
},
|
||||
queryFn: () => fetchDashboardContractMetadata(contract),
|
||||
});
|
||||
}
|
||||
|
||||
type DashboardContractMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
image: string;
|
||||
contractType: string;
|
||||
};
|
||||
|
||||
export async function fetchDashboardContractMetadata(
|
||||
contract: ThirdwebContract,
|
||||
): Promise<DashboardContractMetadata> {
|
||||
const [contractMetadata, _name, _symbol, compilerMetadata] =
|
||||
await Promise.all([
|
||||
getContractMetadata({ contract }).catch(() => ({
|
||||
image: "",
|
||||
name: "",
|
||||
symbol: "",
|
||||
})),
|
||||
name({ contract }).catch(() => ""),
|
||||
symbol({ contract }).catch(() => ""),
|
||||
getCompilerMetadata(contract).catch(() => undefined),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
|
||||
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
|
||||
* In that case, we split the solidity name on ":" and take the second part
|
||||
*/
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
}
|
||||
}
|
||||
|
||||
const contractName =
|
||||
contractMetadata?.name || _name || compilerMetadata?.name || "";
|
||||
const contractSymbol = contractMetadata?.symbol || _symbol || "";
|
||||
|
||||
return {
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
image: contractMetadata.image || "",
|
||||
contractType: compilerMetadata?.name || "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
@@ -5,7 +7,6 @@ import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
|
||||
@@ -17,7 +18,6 @@ export type EngineInstance = {
|
||||
name: string;
|
||||
url: string;
|
||||
lastAccessedAt: string;
|
||||
cloudDeployedAt?: string;
|
||||
status:
|
||||
| "active"
|
||||
| "pending"
|
||||
@@ -25,6 +25,7 @@ export type EngineInstance = {
|
||||
| "deploying"
|
||||
| "paymentFailed"
|
||||
| "deploymentFailed";
|
||||
deploymentId?: string;
|
||||
};
|
||||
|
||||
// Not checking for null token because the token is required the tanstack useQuery hook
|
||||
@@ -188,23 +189,26 @@ export function useEngineLatestVersion() {
|
||||
|
||||
interface UpdateVersionInput {
|
||||
engineId: string;
|
||||
serverVersion: string;
|
||||
}
|
||||
|
||||
export function useEngineUpdateVersion() {
|
||||
export function useEngineUpdateServerVersion() {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateVersionInput) => {
|
||||
invariant(input.engineId, "engineId is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/update-version`, {
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v2/engine/${input.engineId}/infrastructure`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
serverVersion: input.serverVersion,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
engineId: input.engineId,
|
||||
}),
|
||||
});
|
||||
);
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
if (!res.ok) {
|
||||
@@ -570,8 +574,8 @@ export function useEngineAddKeypair(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: AddKeypairInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: AddKeypairInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/keypair/add`, {
|
||||
@@ -587,14 +591,12 @@ export function useEngineAddKeypair(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RemoveKeypairInput = {
|
||||
@@ -605,8 +607,8 @@ export function useEngineRemoveKeypair(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RemoveKeypairInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RemoveKeypairInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/keypair/remove`, {
|
||||
@@ -622,14 +624,12 @@ export function useEngineRemoveKeypair(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type EngineRelayer = {
|
||||
@@ -672,8 +672,8 @@ export function useEngineCreateRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/create`, {
|
||||
@@ -689,14 +689,12 @@ export function useEngineCreateRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeRelayerInput = {
|
||||
@@ -707,8 +705,8 @@ export function useEngineRevokeRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/revoke`, {
|
||||
@@ -724,14 +722,12 @@ export function useEngineRevokeRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type UpdateRelayerInput = {
|
||||
@@ -747,8 +743,8 @@ export function useEngineUpdateRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/update`, {
|
||||
@@ -764,14 +760,12 @@ export function useEngineUpdateRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineWebhook {
|
||||
@@ -827,8 +821,8 @@ export function useEngineSetWalletConfig(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: SetWalletConfigInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: SetWalletConfigInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}configuration/wallets`, {
|
||||
@@ -844,14 +838,12 @@ export function useEngineSetWalletConfig(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletConfig(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletConfig(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateBackendWalletInput = {
|
||||
@@ -862,8 +854,8 @@ export function useEngineCreateBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/create`, {
|
||||
@@ -879,14 +871,12 @@ export function useEngineCreateBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateBackendWalletInput {
|
||||
@@ -898,8 +888,8 @@ export function useEngineUpdateBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/update`, {
|
||||
@@ -915,14 +905,12 @@ export function useEngineUpdateBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type ImportBackendWalletInput =
|
||||
@@ -949,8 +937,8 @@ export function useEngineImportBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: ImportBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ImportBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/import`, {
|
||||
@@ -966,22 +954,20 @@ export function useEngineImportBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineGrantPermissions(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: EngineAdmin) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: EngineAdmin) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/permissions/grant`, {
|
||||
@@ -997,14 +983,12 @@ export function useEngineGrantPermissions(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokePermissionsInput = {
|
||||
@@ -1015,8 +999,8 @@ export function useEngineRevokePermissions(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokePermissionsInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokePermissionsInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/permissions/revoke`, {
|
||||
@@ -1032,14 +1016,12 @@ export function useEngineRevokePermissions(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type CreateAccessTokenResponse = AccessToken & {
|
||||
@@ -1050,8 +1032,8 @@ export function useEngineCreateAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/create`, {
|
||||
@@ -1067,14 +1049,12 @@ export function useEngineCreateAccessToken(instance: string) {
|
||||
|
||||
return json.result as CreateAccessTokenResponse;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeAccessTokenInput = {
|
||||
@@ -1085,8 +1065,8 @@ export function useEngineRevokeAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeAccessTokenInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeAccessTokenInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/revoke`, {
|
||||
@@ -1102,14 +1082,12 @@ export function useEngineRevokeAccessToken(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateAccessTokenInput = {
|
||||
@@ -1121,8 +1099,8 @@ export function useEngineUpdateAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateAccessTokenInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccessTokenInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/update`, {
|
||||
@@ -1138,14 +1116,12 @@ export function useEngineUpdateAccessToken(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateWebhookInput = {
|
||||
@@ -1158,8 +1134,8 @@ export function useEngineCreateWebhook(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateWebhookInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateWebhookInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}webhooks/create`, {
|
||||
@@ -1175,14 +1151,12 @@ export function useEngineCreateWebhook(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeWebhookInput = {
|
||||
@@ -1193,8 +1167,8 @@ export function useEngineRevokeWebhook(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeWebhookInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeWebhookInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}webhooks/revoke`, {
|
||||
@@ -1210,14 +1184,12 @@ export function useEngineRevokeWebhook(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type SendTokensInput = {
|
||||
@@ -1659,8 +1631,8 @@ export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateNotificationChannelInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateNotificationChannelInput) => {
|
||||
invariant(isLoggedIn, "Must be logged in.");
|
||||
|
||||
const res = await fetch(
|
||||
@@ -1677,22 +1649,20 @@ export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
const json = await res.json();
|
||||
return json.data as EngineNotificationChannel;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (notificationChannelId: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (notificationChannelId: string) => {
|
||||
invariant(isLoggedIn, "Must be logged in.");
|
||||
|
||||
const res = await fetch(
|
||||
@@ -1704,12 +1674,10 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
}
|
||||
res.body?.cancel();
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { isLoginRequired } from "@/constants/auth";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]";
|
||||
import { useQueryWithNetwork } from "./query/useQueryWithNetwork";
|
||||
|
||||
export function useWalletNFTs(walletAddress?: string, chainId?: number) {
|
||||
return useQueryWithNetwork(
|
||||
["walletNfts", walletAddress],
|
||||
async () => {
|
||||
export function useWalletNFTs(params: {
|
||||
chainId: number;
|
||||
walletAddress?: string;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: ["walletNfts", params.chainId, params.walletAddress],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(
|
||||
`/api/wallet/nfts/${chainId}?owner=${walletAddress}`,
|
||||
`/api/wallet/nfts/${params.chainId}?owner=${params.walletAddress}`,
|
||||
);
|
||||
return (await response.json()) as WalletNFTApiReturn;
|
||||
},
|
||||
{
|
||||
enabled: !!walletAddress && !!chainId,
|
||||
},
|
||||
);
|
||||
enabled: !!params.walletAddress,
|
||||
});
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckIcon, CircleAlertIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ConfigureNetworks } from "../../../../../components/configure-networks/ConfigureNetworks";
|
||||
import { addChainOverrides } from "../../../../../stores/chainStores";
|
||||
|
||||
export function ConfigureCustomChain(props: {
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const { chainSlug } = props;
|
||||
const isSlugNumber = Number.isInteger(Number.parseInt(chainSlug));
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
|
||||
if (isConfigured) {
|
||||
return (
|
||||
<div className="mx-auto max-w-[600px] py-10">
|
||||
<Alert variant="info">
|
||||
<CheckIcon className="size-5" />
|
||||
<AlertTitle>Network Configured Successfully</AlertTitle>
|
||||
</Alert>
|
||||
<div className="h-6" />
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reload Page to Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[600px] py-10">
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>
|
||||
No Chain found with {isSlugNumber ? "chain ID" : "slug"}{" "}
|
||||
{`"${chainSlug}"`}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Configure a chain with {isSlugNumber ? "chain ID" : "slug"}{" "}
|
||||
{`"${chainSlug}"`} to continue
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="h-6" />
|
||||
<div className="rounded-lg border border-border">
|
||||
<ConfigureNetworks
|
||||
prefillSlug={isSlugNumber ? undefined : chainSlug}
|
||||
prefillChainId={isSlugNumber ? chainSlug : undefined}
|
||||
onNetworkAdded={(network) => {
|
||||
if (
|
||||
chainSlug === network.slug ||
|
||||
chainSlug === `${network.chainId}`
|
||||
) {
|
||||
addChainOverrides(network);
|
||||
setIsConfigured(true);
|
||||
}
|
||||
}}
|
||||
editChain={undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
|
||||
export function isERC721ClaimConditionsSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
// reads
|
||||
ERC721Ext.isGetClaimConditionByIdSupported(functionSelectors),
|
||||
ERC721Ext.isGetActiveClaimConditionIdSupported(functionSelectors),
|
||||
ERC721Ext.isGetClaimConditionsSupported(functionSelectors),
|
||||
ERC721Ext.isGetActiveClaimConditionSupported(functionSelectors),
|
||||
// writes
|
||||
ERC721Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC721Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
export function isERC20ClaimConditionsSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
// reads
|
||||
ERC20Ext.isGetClaimConditionByIdSupported(functionSelectors),
|
||||
ERC20Ext.isGetActiveClaimConditionIdSupported(functionSelectors),
|
||||
ERC20Ext.isGetClaimConditionsSupported(functionSelectors),
|
||||
ERC20Ext.isGetActiveClaimConditionSupported(functionSelectors),
|
||||
// writes
|
||||
ERC20Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC20Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
function isERC1155ClaimConditionsSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
// reads
|
||||
ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors),
|
||||
ERC1155Ext.isGetClaimConditionsSupported(functionSelectors),
|
||||
ERC1155Ext.isGetActiveClaimConditionSupported(functionSelectors),
|
||||
// writes
|
||||
ERC1155Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC1155Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
export function isClaimConditionsSupported(functionSelectors: string[]) {
|
||||
return (
|
||||
isERC721ClaimConditionsSupported(functionSelectors) ||
|
||||
isERC20ClaimConditionsSupported(functionSelectors) ||
|
||||
isERC1155ClaimConditionsSupported(functionSelectors)
|
||||
);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
|
||||
|
||||
export function isAccountFactoryContract(functionSelectors: string[]) {
|
||||
return [
|
||||
ERC4337Ext.isGetAllAccountsSupported(functionSelectors),
|
||||
ERC4337Ext.isGetAccountsSupported(functionSelectors),
|
||||
ERC4337Ext.isTotalAccountsSupported(functionSelectors),
|
||||
ERC4337Ext.isGetAccountsOfSignerSupported(functionSelectors),
|
||||
ERC4337Ext.isPredictAccountAddressSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
export function isAccountContract(functionSelectors: string[]) {
|
||||
return ERC4337Ext.isValidateUserOpSupported(functionSelectors);
|
||||
}
|
||||
|
||||
export function isAccountPermissionsSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
ERC4337Ext.isGetAllActiveSignersSupported(functionSelectors),
|
||||
ERC4337Ext.isGetAllAdminsSupported(functionSelectors),
|
||||
ERC4337Ext.isGetAllSignersSupported(functionSelectors),
|
||||
ERC4337Ext.isIsActiveSignerSupported(functionSelectors),
|
||||
ERC4337Ext.isIsAdminSupported(functionSelectors),
|
||||
ERC4337Ext.isAddAdminSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import * as MarketplaceExt from "thirdweb/extensions/marketplace";
|
||||
|
||||
export function isDirectListingSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
MarketplaceExt.isCreateListingSupported(functionSelectors),
|
||||
MarketplaceExt.isCancelListingSupported(functionSelectors),
|
||||
MarketplaceExt.isBuyFromListingSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
export function isEnglishAuctionSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
MarketplaceExt.isCreateAuctionSupported(functionSelectors),
|
||||
MarketplaceExt.isCancelAuctionSupported(functionSelectors),
|
||||
MarketplaceExt.isBidInAuctionSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
isGetInstalledModulesSupported,
|
||||
isInstallModuleSupported,
|
||||
} from "thirdweb/modules";
|
||||
|
||||
export function isModularCoreContract(functionSelectors: string[]) {
|
||||
return [
|
||||
isGetInstalledModulesSupported(functionSelectors),
|
||||
isInstallModuleSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import * as PermissionsExt from "thirdweb/extensions/permissions";
|
||||
|
||||
export function isPermissionsSupported(functionSelectors: string[]) {
|
||||
return [
|
||||
PermissionsExt.isGetRoleAdminSupported(functionSelectors),
|
||||
PermissionsExt.isGrantRoleSupported(functionSelectors),
|
||||
PermissionsExt.isHasRoleSupported(functionSelectors),
|
||||
PermissionsExt.isRenounceRoleSupported(functionSelectors),
|
||||
PermissionsExt.isRevokeRoleSupported(functionSelectors),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
export function isPermissionsEnumerableSupported(functionSelectors: string[]) {
|
||||
return (
|
||||
isPermissionsSupported(functionSelectors) &&
|
||||
[
|
||||
PermissionsExt.isGetRoleMemberSupported(functionSelectors),
|
||||
PermissionsExt.isGetRoleMemberCountSupported(functionSelectors),
|
||||
].every(Boolean)
|
||||
);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { isERC20 as isERC20_ERC20 } from "thirdweb/extensions/erc20";
|
||||
import { isGetNFTSupported as isGetNFTSupported_ERC721 } from "thirdweb/extensions/erc721";
|
||||
import { isGetNFTSupported as isGetNFTSupported_ERC1155 } from "thirdweb/extensions/erc1155";
|
||||
|
||||
export function supportedERCs(functionSelectors: string[]) {
|
||||
const isERC721 = isGetNFTSupported_ERC721(functionSelectors);
|
||||
const isERC1155 = isGetNFTSupported_ERC1155(functionSelectors);
|
||||
const isERC20 = isERC20_ERC20(functionSelectors);
|
||||
return {
|
||||
isERC721,
|
||||
isERC1155,
|
||||
isERC20,
|
||||
};
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { cookies } from "next/headers";
|
||||
import { TW_LOCAL_CHAIN_STORE } from "stores/storageKeys";
|
||||
import { getAddress, getContract, isAddress } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { fetchChain } from "utils/fetchChain";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
|
||||
export async function getContractPageParamsInfo(params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}) {
|
||||
const contractAddress = getCheckSummedAddress(params.contractAddress);
|
||||
const chainSlugOrId = params.chain_id;
|
||||
let chainMetadata: ChainMetadata | null = null;
|
||||
try {
|
||||
chainMetadata = await fetchChain(chainSlugOrId);
|
||||
} catch {
|
||||
// move on
|
||||
}
|
||||
|
||||
if (!chainMetadata) {
|
||||
const cookieStore = cookies();
|
||||
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
|
||||
|
||||
if (localChainStoreValue) {
|
||||
try {
|
||||
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
|
||||
if (typeof chains === "object" && Array.isArray(chains)) {
|
||||
const chainOverrides = chains as ChainMetadata[];
|
||||
const chain = chainOverrides.find(
|
||||
(c) =>
|
||||
c.slug === chainSlugOrId ||
|
||||
c.chainId === Number.parseInt(chainSlugOrId),
|
||||
);
|
||||
if (chain) {
|
||||
chainMetadata = chain;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!chainMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// attempt to get the auth token
|
||||
const token = getAuthToken();
|
||||
|
||||
const contract = getContract({
|
||||
address: contractAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chainMetadata),
|
||||
// if we have the auth token pass it into the client
|
||||
client: getThirdwebClient(token || undefined),
|
||||
});
|
||||
|
||||
return {
|
||||
chainMetadata,
|
||||
contract,
|
||||
};
|
||||
}
|
||||
|
||||
function getCheckSummedAddress(address: string) {
|
||||
return isAddress(address) ? getAddress(address) : address;
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
|
||||
import {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
} from "./detectedFeatures/claimConditions";
|
||||
import {
|
||||
isAccountContract,
|
||||
isAccountFactoryContract,
|
||||
isAccountPermissionsSupported,
|
||||
} from "./detectedFeatures/erc4337";
|
||||
import {
|
||||
isDirectListingSupported,
|
||||
isEnglishAuctionSupported,
|
||||
} from "./detectedFeatures/marketplace";
|
||||
import { isModularCoreContract } from "./detectedFeatures/modular";
|
||||
import {
|
||||
isPermissionsEnumerableSupported,
|
||||
isPermissionsSupported,
|
||||
} from "./detectedFeatures/permissions";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
import { isAnalyticsSupportedForChain } from "./isAnalyticsSupportedForChain";
|
||||
|
||||
export type ContractPageMetadata = {
|
||||
supportedERCs: {
|
||||
isERC721: boolean;
|
||||
isERC1155: boolean;
|
||||
isERC20: boolean;
|
||||
};
|
||||
isDirectListingSupported: boolean;
|
||||
isEnglishAuctionSupported: boolean;
|
||||
isPermissionsSupported: boolean;
|
||||
isVoteContract: boolean;
|
||||
isPermissionsEnumerableSupported: boolean;
|
||||
isModularCore: boolean;
|
||||
embedType: EmbedTypeToShow;
|
||||
isAnalyticsSupported: boolean;
|
||||
isSplitSupported: boolean;
|
||||
isERC721ClaimConditionsSupported: boolean;
|
||||
isERC20ClaimConditionsSupported: boolean;
|
||||
isAccountFactory: boolean;
|
||||
isAccount: boolean;
|
||||
isAccountPermissionsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
};
|
||||
|
||||
export async function getContractPageMetadata(
|
||||
contract: ThirdwebContract,
|
||||
): Promise<ContractPageMetadata> {
|
||||
const [
|
||||
functionSelectorsResult,
|
||||
isAnalyticsSupportedResult,
|
||||
contractTypeResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
isAnalyticsSupportedForChain(contract.chain.id),
|
||||
getContractType({ contract }),
|
||||
]);
|
||||
|
||||
const functionSelectors =
|
||||
functionSelectorsResult.status === "fulfilled"
|
||||
? functionSelectorsResult.value
|
||||
: [];
|
||||
|
||||
const isAnalyticsSupported =
|
||||
isAnalyticsSupportedResult.status === "fulfilled"
|
||||
? isAnalyticsSupportedResult.value
|
||||
: true;
|
||||
|
||||
const contractType =
|
||||
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
|
||||
|
||||
return {
|
||||
supportedERCs: supportedERCs(functionSelectors),
|
||||
isDirectListingSupported: isDirectListingSupported(functionSelectors),
|
||||
isEnglishAuctionSupported: isEnglishAuctionSupported(functionSelectors),
|
||||
isPermissionsSupported: isPermissionsSupported(functionSelectors),
|
||||
isPermissionsEnumerableSupported:
|
||||
isPermissionsEnumerableSupported(functionSelectors),
|
||||
isModularCore: isModularCoreContract(functionSelectors),
|
||||
embedType: getEmbedTypeToShow(functionSelectors),
|
||||
isAnalyticsSupported: isAnalyticsSupported,
|
||||
isSplitSupported: contractType === "Split",
|
||||
isVoteContract: contractType === "VoteERC20",
|
||||
isERC721ClaimConditionsSupported:
|
||||
isERC721ClaimConditionsSupported(functionSelectors),
|
||||
isERC20ClaimConditionsSupported:
|
||||
isERC20ClaimConditionsSupported(functionSelectors),
|
||||
isAccountFactory: isAccountFactoryContract(functionSelectors),
|
||||
isAccount: isAccountContract(functionSelectors),
|
||||
isAccountPermissionsSupported:
|
||||
isAccountPermissionsSupported(functionSelectors),
|
||||
functionSelectors,
|
||||
};
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import type { SidebarBaseLink, SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import type { ContractPageMetadata } from "./getContractPageMetadata";
|
||||
|
||||
type SidebarBaseLinkWithHide = SidebarBaseLink & {
|
||||
hide?: boolean;
|
||||
};
|
||||
|
||||
export function getContractPageSidebarLinks(data: {
|
||||
metadata: ContractPageMetadata;
|
||||
contractAddress: string;
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const layoutPrefix = `/${data.chainSlug}/${data.contractAddress}`;
|
||||
|
||||
const generalLinks: SidebarBaseLinkWithHide[] = [
|
||||
{
|
||||
label: "Overview",
|
||||
href: layoutPrefix,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Modules",
|
||||
href: `${layoutPrefix}/modules`,
|
||||
hide: !data.metadata.isModularCore,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Code Snippets",
|
||||
href: `${layoutPrefix}/code`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Explorer",
|
||||
href: `${layoutPrefix}/explorer`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Events",
|
||||
href: `${layoutPrefix}/events`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Embed",
|
||||
href: `${layoutPrefix}/embed`,
|
||||
hide: data.metadata.embedType === null,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
href: `${layoutPrefix}/analytics`,
|
||||
hide: !data.metadata.isAnalyticsSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
href: `${layoutPrefix}/settings`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Sources",
|
||||
href: `${layoutPrefix}/sources`,
|
||||
exactMatch: true,
|
||||
},
|
||||
];
|
||||
|
||||
const extensionsLinks: SidebarBaseLinkWithHide[] = [
|
||||
{
|
||||
label: "NFTs",
|
||||
href: `${layoutPrefix}/nfts`,
|
||||
hide:
|
||||
!data.metadata.supportedERCs.isERC721 &&
|
||||
!data.metadata.supportedERCs.isERC1155,
|
||||
},
|
||||
{
|
||||
label: "Tokens",
|
||||
href: `${layoutPrefix}/tokens`,
|
||||
hide: !data.metadata.supportedERCs.isERC20,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Direct Listings",
|
||||
href: `${layoutPrefix}/direct-listings`,
|
||||
hide: !data.metadata.isDirectListingSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "English Auctions",
|
||||
href: `${layoutPrefix}/english-auctions`,
|
||||
hide: !data.metadata.isEnglishAuctionSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Balances",
|
||||
href: `${layoutPrefix}/split`,
|
||||
hide: !data.metadata.isSplitSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Proposals",
|
||||
href: `${layoutPrefix}/proposals`,
|
||||
hide: !data.metadata.isVoteContract,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Claim Conditions",
|
||||
href: `${layoutPrefix}/claim-conditions`,
|
||||
hide: !(
|
||||
data.metadata.isERC721ClaimConditionsSupported ||
|
||||
data.metadata.isERC20ClaimConditionsSupported
|
||||
),
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Accounts",
|
||||
href: `${layoutPrefix}/accounts`,
|
||||
hide: !data.metadata.isAccountFactory,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Balance",
|
||||
href: `${layoutPrefix}/account`,
|
||||
hide: !data.metadata.isAccount,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Account Permissions",
|
||||
href: `${layoutPrefix}/account-permissions`,
|
||||
hide: !data.metadata.isAccountPermissionsSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Permissions",
|
||||
href: `${layoutPrefix}/permissions`,
|
||||
exactMatch: true,
|
||||
hide: !data.metadata.isPermissionsSupported,
|
||||
},
|
||||
];
|
||||
|
||||
const extensionsToShow = extensionsLinks.filter((l) => !l.hide);
|
||||
const generalLinksToShow = generalLinks.filter((l) => !l.hide);
|
||||
|
||||
const sidebarLinks: SidebarLink[] = [
|
||||
{
|
||||
group: "General",
|
||||
links: generalLinksToShow,
|
||||
},
|
||||
];
|
||||
|
||||
if (extensionsToShow.length > 0) {
|
||||
sidebarLinks.push({
|
||||
group: "Extensions",
|
||||
links: extensionsToShow,
|
||||
});
|
||||
}
|
||||
|
||||
return sidebarLinks;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { isClaimConditionsSupported } from "./detectedFeatures/claimConditions";
|
||||
import {
|
||||
isDirectListingSupported,
|
||||
isEnglishAuctionSupported,
|
||||
} from "./detectedFeatures/marketplace";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
|
||||
export type EmbedTypeToShow =
|
||||
| "marketplace-v3"
|
||||
| "erc20"
|
||||
| "erc1155"
|
||||
| "erc721"
|
||||
| null;
|
||||
|
||||
export function getEmbedTypeToShow(
|
||||
functionSelectors: string[],
|
||||
): EmbedTypeToShow {
|
||||
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
|
||||
|
||||
if (
|
||||
isEnglishAuctionSupported(functionSelectors) &&
|
||||
isDirectListingSupported(functionSelectors)
|
||||
) {
|
||||
return "marketplace-v3";
|
||||
}
|
||||
|
||||
if (isClaimConditionsSupported(functionSelectors)) {
|
||||
if (isERC721) {
|
||||
return "erc721";
|
||||
}
|
||||
if (isERC1155) {
|
||||
return "erc1155";
|
||||
}
|
||||
if (isERC20) {
|
||||
return "erc20";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { isProd } from "@/constants/env";
|
||||
|
||||
export async function isAnalyticsSupportedForChain(
|
||||
chainId: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!process.env.CHAINSAW_API_KEY) {
|
||||
throw new Error("Missing CHAINSAW_API_KEY env var");
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`https://chainsaw.${isProd ? "thirdweb" : "thirdweb-dev"}.com/service/chains/${chainId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
// pass the shared secret
|
||||
"x-service-api-key": process.env.CHAINSAW_API_KEY || "",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
// assume not supported if we get a non-200 response
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data } = await res.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("Error checking if analytics is supported for chain", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AccountSigners } from "../../../../../../contract-ui/tabs/account-permissions/components/account-signers";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
Account Signers
|
||||
</h1>
|
||||
<AccountSigners contract={contract} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AccountPage } from "../../../../../../contract-ui/tabs/account/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { isAccount } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccount) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountPage contract={contract} chainMetadata={chainMetadata} />;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AccountsPage } from "../../../../../../contract-ui/tabs/accounts/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { isAccountFactory } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountsPage contract={contract} />;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractAnalyticsPage } from "../../../../../../contract-ui/tabs/analytics/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isAnalyticsSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractAnalyticsPage contract={info.contract} />;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ClaimConditions } from "../../../../../../contract-ui/tabs/claim-conditions/components/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
} = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimConditions contract={contract} isERC20={supportedERCs.isERC20} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
try {
|
||||
const abi = await resolveContractAbi(contract);
|
||||
|
||||
return (
|
||||
<CodeOverview
|
||||
abi={abi}
|
||||
contractAddress={contract.address}
|
||||
chainId={contract.chain.id}
|
||||
/>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractDirectListingsPage } from "../../../../../../contract-ui/tabs/direct-listings/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isDirectListingSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractDirectListingsPage contract={info.contract} />;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { EmbedSetup } from "../../../../../../contract-ui/tabs/embed/components/embed-setup";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (embedType === null) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <EmbedSetup contract={info.contract} ercOrMarketplace={embedType} />;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractEnglishAuctionsPage } from "../../../../../../contract-ui/tabs/english-auctions/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
if (!isEnglishAuctionSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractEnglishAuctionsPage contract={info.contract} />;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { EventsFeed } from "../../../../../../contract-ui/tabs/events/components/events-feed";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <EventsFeed contract={info.contract} />;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { ContractExplorerPage } from "../../../../../../contract-ui/tabs/explorer/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
try {
|
||||
const abi = await resolveContractAbi(contract);
|
||||
return <ContractExplorerPage contract={contract} abi={abi} />;
|
||||
} catch {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import { PrimaryDashboardButton } from "contract-ui/components/primary-dashboard-button";
|
||||
import type { Metadata } from "next";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
|
||||
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
|
||||
import { ConfigureCustomChain } from "./ConfigureCustomChain";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
return <ConfigureCustomChain chainSlug={props.params.chain_id} />;
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const contractPageMetadata = await getContractPageMetadata(contract);
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
metadata: contractPageMetadata,
|
||||
});
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SidebarLayout sidebarLinks={sidebarLinks}>
|
||||
<div className="border-border border-b pb-8">
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
<ContractMetadata contract={contract} chain={chainMetadata} />
|
||||
<PrimaryDashboardButton
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
chain: chainMetadata,
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8" />
|
||||
<div className="pb-10">{props.children}</div>
|
||||
</SidebarLayout>
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}): Promise<Metadata> {
|
||||
try {
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const [functionSelectors, contractMetadata] = await Promise.all([
|
||||
resolveFunctionSelectors(info.contract),
|
||||
getContractMetadata({
|
||||
contract: info.contract,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (contractMetadata.name === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
|
||||
|
||||
const contractDisplayName = `${contractMetadata.name}${
|
||||
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
|
||||
}`;
|
||||
|
||||
const cleanedChainName = info?.chainMetadata?.name
|
||||
.replace("Mainnet", "")
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
description: description,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chain_id}`,
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chain_id}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractEditModulesPage } from "../../../../../../contract-ui/tabs/manage/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractEditModulesPage contract={info.contract} />;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { TokenIdPage } from "../../../../../../../contract-ui/tabs/nfts/components/token-id";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
tokenId: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { supportedERCs } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
if (!isOnlyNumbers(props.params.tokenId)) {
|
||||
// redirect to nfts index page
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
|
||||
}
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={contract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
tokenId={props.params.tokenId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractNFTPage } from "../../../../../../contract-ui/tabs/nfts/page";
|
||||
import { resolveFunctionSelectors } from "../../../../../../lib/selectors";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { supportedERCs } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
const functionSelectors = await resolveFunctionSelectors(contract);
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={contract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+82
-35
@@ -1,27 +1,60 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { isProd } from "@/constants/env";
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { ContractOG } from "og-lib/url-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
|
||||
export function shortenString(str: string) {
|
||||
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
|
||||
}
|
||||
export const runtime = "edge";
|
||||
|
||||
// Make sure the font exists in the specified path:
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
|
||||
const inter400_ = fetch(
|
||||
new URL("og-lib/fonts/inter/400.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
const inter500_ = fetch(
|
||||
new URL("og-lib/fonts/inter/500.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
const inter700_ = fetch(
|
||||
new URL("og-lib/fonts/inter/700.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
try {
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const contractMetadata = await getContractMetadata({
|
||||
contract: info.contract,
|
||||
});
|
||||
|
||||
if (contractMetadata.name === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const contractDisplayName = `${contractMetadata.name}${
|
||||
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
|
||||
}`;
|
||||
|
||||
return contractOGImageTemplate({
|
||||
displayName: contractDisplayName,
|
||||
contractAddress: info.contract.address,
|
||||
chainName: info.chainMetadata.name,
|
||||
logo: contractMetadata.image,
|
||||
});
|
||||
} catch {
|
||||
return contractOGImageTemplate({
|
||||
contractAddress: params.contractAddress,
|
||||
chainName: params.chain_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shortenString(str: string) {
|
||||
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
|
||||
}
|
||||
|
||||
const OgBrandIcon: React.FC = () => (
|
||||
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
|
||||
@@ -60,9 +93,8 @@ const OgBrandIcon: React.FC = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const MAX_LENGTH = 50;
|
||||
|
||||
function textShortener(text: string) {
|
||||
const MAX_LENGTH = 50;
|
||||
const words = [];
|
||||
let currentLength = 0;
|
||||
let shortened = false;
|
||||
@@ -108,12 +140,21 @@ function replaceAnyIpfsUrlWithGateway(url: string) {
|
||||
return url;
|
||||
}
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
if (req.method !== "GET") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const contractData = ContractOG.fromUrl(new URL(req.url));
|
||||
async function contractOGImageTemplate(params: {
|
||||
logo?: string;
|
||||
displayName?: string;
|
||||
contractAddress: string;
|
||||
chainName: string;
|
||||
}) {
|
||||
const inter400_ = fetch(
|
||||
new URL("../../../../../og-lib/fonts/inter/400.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
const inter500_ = fetch(
|
||||
new URL("../../../../../og-lib/fonts/inter/500.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
const inter700_ = fetch(
|
||||
new URL("../../../../../og-lib/fonts/inter/700.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
const [inter400, inter500, inter700] = await Promise.all([
|
||||
inter400_,
|
||||
@@ -121,6 +162,10 @@ export default async function handler(req: NextRequest) {
|
||||
inter700_,
|
||||
]);
|
||||
|
||||
const title =
|
||||
(params.displayName ? textShortener(params.displayName) : "") ||
|
||||
shortenString(params.contractAddress);
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
tw="w-full h-full flex justify-center py-8 px-8 "
|
||||
@@ -133,31 +178,33 @@ export default async function handler(req: NextRequest) {
|
||||
|
||||
<div tw="flex flex-col my-auto w-full">
|
||||
{/* if we have a logo render it */}
|
||||
{contractData.logo && (
|
||||
{params.logo && (
|
||||
<img
|
||||
alt=""
|
||||
tw="w-50 h-50 rounded-lg mb-8 flex mx-auto"
|
||||
src={replaceAnyIpfsUrlWithGateway(contractData.logo)}
|
||||
src={replaceAnyIpfsUrlWithGateway(params.logo)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{contractData.displayName &&
|
||||
contractData.displayName !== contractData.contractAddress ? (
|
||||
{params.displayName && params.displayName !== params.contractAddress ? (
|
||||
<h3
|
||||
tw="text-2xl font-bold flex text-center my-0 mb-4 mx-auto"
|
||||
style={{
|
||||
color: "#626262",
|
||||
}}
|
||||
>
|
||||
{shortenString(contractData.contractAddress)}
|
||||
{shortenString(params.contractAddress)}
|
||||
</h3>
|
||||
) : null}
|
||||
<h1 tw="text-6xl text-white font-bold flex text-center my-0 mx-auto">
|
||||
{textShortener(contractData.displayName) ||
|
||||
shortenString(contractData.contractAddress)}
|
||||
</h1>
|
||||
|
||||
{title && (
|
||||
<h1 tw="text-6xl text-white font-bold flex text-center my-0 mx-auto">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
<h2 tw="text-4xl text-white opacity-80 font-medium flex text-center my-0 mt-10 mx-auto">
|
||||
{contractData.chainName}
|
||||
{params.chainName}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ContractOverviewPage } from "contract-ui/tabs/overview/page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const contractPageMetadata = await getContractPageMetadata(contract);
|
||||
|
||||
return (
|
||||
<ContractOverviewPage
|
||||
contract={contract}
|
||||
hasDirectListings={contractPageMetadata.isDirectListingSupported}
|
||||
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
|
||||
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
|
||||
isErc20={contractPageMetadata.supportedERCs.isERC20}
|
||||
isErc721={contractPageMetadata.supportedERCs.isERC721}
|
||||
isPermissionsEnumerable={
|
||||
contractPageMetadata.isPermissionsEnumerableSupported
|
||||
}
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContractPermissionsPage } from "../../../../../../contract-ui/tabs/permissions/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { isPermissionsEnumerableSupported } =
|
||||
await getContractPageMetadata(contract);
|
||||
|
||||
return (
|
||||
<ContractPermissionsPage
|
||||
contract={contract}
|
||||
chainSlug={info.chainMetadata.slug}
|
||||
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractProposalsPage } from "../../../../../../contract-ui/tabs/proposals/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isVoteContract } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isVoteContract) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractProposalsPage contract={info.contract} />;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import * as CommonExt from "thirdweb/extensions/common";
|
||||
import { ContractSettingsPage } from "../../../../../../contract-ui/tabs/settings/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { functionSelectors } = await getContractPageMetadata(info.contract);
|
||||
|
||||
return (
|
||||
<ContractSettingsPage
|
||||
contract={info.contract}
|
||||
isContractMetadataSupported={[
|
||||
CommonExt.isGetContractMetadataSupported(functionSelectors),
|
||||
CommonExt.isSetContractMetadataSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPrimarySaleSupported={[
|
||||
CommonExt.isPrimarySaleRecipientSupported(functionSelectors),
|
||||
CommonExt.isSetPrimarySaleRecipientSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isRoyaltiesSupported={[
|
||||
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPlatformFeesSupported={[
|
||||
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
|
||||
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContractSourcesPage } from "../../../../../../contract-ui/tabs/sources/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <ContractSourcesPage contract={info.contract} />;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractSplitPage } from "../../../../../../contract-ui/tabs/split/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isSplitSupported } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isSplitSupported) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractSplitPage contract={info.contract} />;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
isClaimToSupported,
|
||||
isMintToSupported,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import { ContractTokensPage } from "../../../../../../contract-ui/tabs/tokens/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
return (
|
||||
<ContractTokensPage
|
||||
contract={info.contract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isMintToSupported={isMintToSupported(functionSelectors)}
|
||||
isClaimToSupported={isClaimToSupported(functionSelectors)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import zytronCTA from "./temp-assets/zytronCTA.jpg";
|
||||
|
||||
// END TEMPORARY
|
||||
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type {
|
||||
ChainMetadataWithServices,
|
||||
@@ -44,18 +45,15 @@ import type {
|
||||
ChainServices,
|
||||
} from "./types/chain";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export async function getChains() {
|
||||
const [chains, chainServices] = await Promise.all([
|
||||
fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/chains`,
|
||||
`${API_SERVER_URL}/v1/chains`,
|
||||
// revalidate every 60 minutes
|
||||
{ next: { revalidate: 60 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{ data: ChainMetadata[] }>,
|
||||
fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/chains/services`,
|
||||
`${API_SERVER_URL}/v1/chains/services`,
|
||||
// revalidate every 60 minutes
|
||||
{ next: { revalidate: 60 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{
|
||||
@@ -77,12 +75,12 @@ export async function getChain(
|
||||
): Promise<ChainMetadataWithServices> {
|
||||
const [chain, chainServices] = await Promise.all([
|
||||
fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}`,
|
||||
`${API_SERVER_URL}/v1/chains/${chainIdOrSlug}`,
|
||||
// revalidate every 15 minutes
|
||||
{ next: { revalidate: 15 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{ data: ChainMetadata }>,
|
||||
fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}/services`,
|
||||
`${API_SERVER_URL}/v1/chains/${chainIdOrSlug}/services`,
|
||||
// revalidate every 15 minutes
|
||||
{ next: { revalidate: 15 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{ data: ChainServices }>,
|
||||
@@ -237,6 +235,18 @@ const chainMetaRecord = {
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
// Superposition Mainnet
|
||||
55244: {
|
||||
headerImgUrl: superpositionBanner.src,
|
||||
about:
|
||||
"Superposition is the first blockchain that pays you to use it. It is a DeFi native Layer-3 that focuses on novel incentives and order-flow for growth and value capture for developers and users alike.\n\nSuperpositions includes a native on-chain orderbook with faster execution speeds through Stylus, providing shared and permissionless liquidity for all apps onchain, and Super Assets, which pay yield when you both hold and use them.",
|
||||
cta: {
|
||||
backgroundImageUrl: superpositionCTA.src,
|
||||
title: "The Blockchain that pays you to use it",
|
||||
buttonLink: "https://superposition.so/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
// Aleph 0 Mainnet
|
||||
41455: {
|
||||
headerImgUrl: alephZeroBaner.src,
|
||||
|
||||
+5
-7
@@ -1,14 +1,12 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export async function fetchEcosystem(slug: string, authToken: string) {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/${slug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/${slug}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
console.error(data);
|
||||
|
||||
+5
-7
@@ -1,14 +1,12 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export async function fetchEcosystemList(authToken: string) {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/list`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/list`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
|
||||
import { ConnectSidebarLayout } from "./DashboardConnectLayout";
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <ConnectSidebarLayout>{props.children}</ConnectSidebarLayout>;
|
||||
return (
|
||||
<ConnectSidebarLayout>
|
||||
<BillingAlerts className="pb-6" />
|
||||
{props.children}
|
||||
</ConnectSidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { DashboardHeader } from "app/components/Header/DashboardHeader";
|
||||
import { ErrorProvider } from "../../contexts/error-handler";
|
||||
import { DashboardHeader } from "../components/Header/DashboardHeader";
|
||||
|
||||
export default function DashboardLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
+7
@@ -25,6 +25,13 @@ export async function generateMetadata({ params }: { params: Params }) {
|
||||
|
||||
const publishedContract = publishedContracts[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return {
|
||||
title: `${contract_id} | Published Smart Contract`,
|
||||
description: `Deploy ${contract_id} Smart Contract in one click with thirdweb.`,
|
||||
};
|
||||
}
|
||||
|
||||
const publishedContractName =
|
||||
publishedContract?.displayName || publishedContract?.name;
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ChakraProviderSetup } from "../../../../../@/components/ChakraProviderSetup";
|
||||
import { PublishedContract } from "../../../../../components/contract-components/published-contract";
|
||||
import { PublishedActions } from "../../components/contract-actions-published.client";
|
||||
import { DeployContractHeader } from "../../components/contract-header";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -11,8 +12,6 @@ type State = {
|
||||
};
|
||||
|
||||
const UNTHREAD_API_KEY = process.env.UNTHREAD_API_KEY || "";
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
const planToCustomerId = {
|
||||
free: process.env.UNTHREAD_FREE_TIER_ID as string,
|
||||
@@ -77,7 +76,7 @@ export async function createTicketAction(
|
||||
// user is not logged in, make them log in
|
||||
redirect(`/login?next=${encodeURIComponent("/support")}`);
|
||||
}
|
||||
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
|
||||
import { BookOpenIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
import accountIcon from "../../../../public/assets/support/account.svg";
|
||||
import contractsIcon from "../../../../public/assets/support/contracts.png";
|
||||
import discordIllustration from "../../../../public/assets/support/discord-illustration.png";
|
||||
@@ -213,7 +213,7 @@ export default function SupportPage() {
|
||||
asChild
|
||||
>
|
||||
<Link href="https://discord.gg/thirdweb">
|
||||
<SiDiscord />
|
||||
<DiscordIcon className="size-4" />
|
||||
<span>Join Discord</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -35,10 +35,10 @@ export function AccountHeader(props: {
|
||||
|
||||
const headerProps: AccountHeaderCompProps = {
|
||||
teamsAndProjects: props.teamsAndProjects,
|
||||
email: myAccountQuery.data?.email,
|
||||
logout: logout,
|
||||
connectButton: <CustomConnectWallet />,
|
||||
createProject: () => setIsCreateProjectDialogOpen(true),
|
||||
account: myAccountQuery.data,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -45,13 +45,26 @@ function Variants(props: {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex min-h-screen flex-col gap-6 bg-gray-700 px-4 py-10">
|
||||
<BadgeContainer label="Logged in">
|
||||
<BadgeContainer label="Account Loaded">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
email="[email protected]"
|
||||
logout={() => {}}
|
||||
connectButton={<ConnectButtonStub />}
|
||||
createProject={() => {}}
|
||||
account={{
|
||||
id: "foo",
|
||||
email: "[email protected]",
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Account Loading">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
logout={() => {}}
|
||||
connectButton={<ConnectButtonStub />}
|
||||
createProject={() => {}}
|
||||
account={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Team } from "@/api/team";
|
||||
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import Link from "next/link";
|
||||
import { SecondaryNav } from "../../components/Header/SecondaryNav/SecondaryNav";
|
||||
import { MobileBurgerMenuButton } from "../../components/MobileBurgerMenuButton";
|
||||
@@ -10,11 +12,11 @@ import { TeamSelectorMobileMenuButton } from "../../team/components/TeamHeader/T
|
||||
|
||||
export type AccountHeaderCompProps = {
|
||||
className?: string;
|
||||
email: string | undefined;
|
||||
logout: () => void;
|
||||
connectButton: React.ReactNode;
|
||||
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
|
||||
createProject: () => void;
|
||||
account: Pick<Account, "email" | "id"> | undefined;
|
||||
};
|
||||
|
||||
export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
|
||||
@@ -37,8 +39,12 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
|
||||
href="/account"
|
||||
className="flex flex-row items-center gap-2 font-normal text-sm"
|
||||
>
|
||||
{/* TODO - replace with account image */}
|
||||
<div className="size-6 rounded-full border border-border bg-muted" />
|
||||
{/* TODO - set account Image */}
|
||||
<GradientAvatar
|
||||
id={props.account?.id}
|
||||
src={props.account ? "" : undefined}
|
||||
className="size-6"
|
||||
/>
|
||||
<span> My Account </span>
|
||||
</Link>
|
||||
|
||||
@@ -48,12 +54,13 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
focus="team-selection"
|
||||
createProject={props.createProject}
|
||||
account={props.account}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SecondaryNav
|
||||
email={props.email}
|
||||
account={props.account}
|
||||
logout={props.logout}
|
||||
connectButton={props.connectButton}
|
||||
/>
|
||||
@@ -77,8 +84,12 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
|
||||
"flex flex-row items-center gap-2 font-normal text-foreground text-sm",
|
||||
)}
|
||||
>
|
||||
{/* TODO - replace with account image */}
|
||||
<div className="size-7 rounded-full border border-border bg-muted" />
|
||||
{/* TODO - set account image */}
|
||||
<GradientAvatar
|
||||
id={props.account?.id}
|
||||
src={props.account ? "" : undefined}
|
||||
className="size-6"
|
||||
/>
|
||||
<span> My Account </span>
|
||||
</Link>
|
||||
|
||||
@@ -86,12 +97,13 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
|
||||
currentTeam={undefined}
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
upgradeTeamLink={undefined}
|
||||
account={props.account}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MobileBurgerMenuButton
|
||||
email={props.email}
|
||||
email={props.account?.email}
|
||||
logout={props.logout}
|
||||
connectButton={props.connectButton}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function AccountLayout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-col bg-background">
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="flex grow flex-col">
|
||||
<HeaderAndNav />
|
||||
<div className="border-border border-b py-10">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { Team } from "@/api/team";
|
||||
import type { TeamAccountRole } from "@/api/team-members";
|
||||
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -102,8 +103,8 @@ function TeamRow(props: {
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* start */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* TODO - render team avatar */}
|
||||
<div className="size-8 rounded-full border bg-muted" />
|
||||
{/* TODO - set image */}
|
||||
<GradientAvatar className="size-8" src={""} id={props.team.id} />
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { upload } from "thirdweb/storage";
|
||||
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
|
||||
|
||||
export function AccountSettingsPage(props: {
|
||||
account: Account;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<AccountSettingsPageUI
|
||||
account={props.account}
|
||||
updateAccountImage={async (file) => {
|
||||
if (file) {
|
||||
// upload to IPFS
|
||||
const ipfsUri = await upload({
|
||||
client: props.client,
|
||||
files: [file],
|
||||
});
|
||||
|
||||
// TODO - Implement updating the account image with uri
|
||||
console.log(ipfsUri);
|
||||
} else {
|
||||
// TODO - Implement deleting the account image
|
||||
}
|
||||
|
||||
throw new Error("Not implemented");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,9 @@ function Variants() {
|
||||
name: "John Doe",
|
||||
email: "[email protected]",
|
||||
}}
|
||||
updateAccountImage={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
|
||||
@@ -42,10 +42,11 @@ type MinimalAccount = Pick<
|
||||
|
||||
export function AccountSettingsPageUI(props: {
|
||||
account: MinimalAccount;
|
||||
updateAccountImage: (file: File | undefined) => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<AccountAvatarFormControl />
|
||||
<AccountAvatarFormControl updateAccountImage={props.updateAccountImage} />
|
||||
<AccountNameFormControl name={props.account.name || ""} />
|
||||
<AccountEmailFormControl
|
||||
email={props.account.email || ""}
|
||||
@@ -58,16 +59,14 @@ export function AccountSettingsPageUI(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AccountAvatarFormControl() {
|
||||
function AccountAvatarFormControl(props: {
|
||||
updateAccountImage: (file: File | undefined) => Promise<void>;
|
||||
}) {
|
||||
const [avatar, setAvatar] = useState<File>(); // TODO: prefill with account avatar
|
||||
|
||||
// TODO - implement
|
||||
const updateAvatarMutation = useMutation({
|
||||
mutationFn: async (_avatar: File | undefined) => {
|
||||
// Fake loading
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
console.log("Updating account image to", _avatar);
|
||||
throw new Error("Not implemented");
|
||||
await props.updateAccountImage(_avatar);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { getAuthToken } from "../../api/lib/getAuthToken";
|
||||
|
||||
export async function getAccount() {
|
||||
const authToken = getAuthToken();
|
||||
const apiServerURL = new URL(
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
|
||||
);
|
||||
const apiServerURL = new URL(API_SERVER_URL);
|
||||
|
||||
apiServerURL.pathname = "/v1/account/me";
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
|
||||
import { getAuthToken } from "../../api/lib/getAuthToken";
|
||||
import { AccountSettingsPage } from "./AccountSettingsPage";
|
||||
import { getAccount } from "./getAccount";
|
||||
|
||||
export default async function Page() {
|
||||
const account = await getAccount();
|
||||
const token = getAuthToken();
|
||||
|
||||
if (!account) {
|
||||
if (!account || !token) {
|
||||
redirect(`/login?next=${encodeURIComponent("/account")}`);
|
||||
}
|
||||
|
||||
return <AccountSettingsPageUI account={account} />;
|
||||
return (
|
||||
<AccountSettingsPage account={account} client={getThirdwebClient(token)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb/utils";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type EnsureLoginPayload = {
|
||||
pathname: string;
|
||||
address?: string;
|
||||
@@ -56,7 +54,7 @@ export const GET = async (req: NextRequest) => {
|
||||
}
|
||||
|
||||
// check that the token is valid by checking for the user account
|
||||
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb/utils";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type GetAuthTokenResponse = {
|
||||
jwt: string | null;
|
||||
};
|
||||
@@ -43,7 +41,7 @@ export const GET = async (req: NextRequest) => {
|
||||
}
|
||||
|
||||
// check token validity
|
||||
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { getAuthToken } from "./getAuthToken";
|
||||
|
||||
@@ -36,17 +37,14 @@ export async function getAPIKey(apiKeyId: string) {
|
||||
|
||||
export async function getApiKeys() {
|
||||
const authToken = getAuthToken();
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/keys`,
|
||||
{
|
||||
method: "GET",
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/keys`, {
|
||||
method: "GET",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
|
||||
@@ -1,68 +1,101 @@
|
||||
import { ColorModeToggle } from "@/components/color-mode-toggle";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { NavLink } from "@/components/ui/NavLink";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { CmdKSearch } from "components/cmd-k-search";
|
||||
import { ColorModeToggle } from "components/color-mode/color-mode-toggle";
|
||||
import { Logo } from "components/logo";
|
||||
import { CreditsButton } from "components/settings/Account/Billing/CreditsButton";
|
||||
import { UpgradeButton } from "components/settings/Account/Billing/UpgradeButton";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ThirdwebMiniLogo } from "../ThirdwebMiniLogo";
|
||||
import { NavLink } from "../nav-link.client";
|
||||
import { MobileBurgerMenu } from "./MobileBurgerMenu";
|
||||
import { headerLinks } from "./headerLinks";
|
||||
import { Suspense } from "react";
|
||||
import { DashboardHeaderTabs } from "./DashboardHeaderTabs";
|
||||
|
||||
export function DashboardHeader() {
|
||||
export const DashboardHeader: React.FC = () => {
|
||||
return (
|
||||
// the "h-24" avoids layout shift when connecting wallet (connected wallet button is taller than disconnected...)
|
||||
<header className="flex h-20 flex-shrink-0 flex-row items-center justify-between border-border border-b bg-muted/50 px-4 py-4 md:h-20 lg:px-6">
|
||||
{/* Left */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<MobileBurgerMenu />
|
||||
<div className="bg-muted/50">
|
||||
<header className="flex items-center justify-between px-4 py-3 lg:px-6">
|
||||
{/* left */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard">
|
||||
<ThirdwebMiniLogo className="h-6" />
|
||||
<Logo hideWordmark />
|
||||
</Link>
|
||||
<CmdKSearch />
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
{headerLinks.left.map((link) => (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.href.startsWith("https") ? "_blank" : undefined}
|
||||
className="px-[10px] py-2 font-normal text-muted-foreground text-sm hover:text-foreground"
|
||||
activeClassName="font-medium text-foreground"
|
||||
>
|
||||
{link.name}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Right */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="items-center gap-2 lg:flex">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
{headerLinks.right.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.href.startsWith("https") ? "_blank" : undefined}
|
||||
className="px-[10px] py-2 font-normal text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
{/* Right */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="hidden gap-2 md:flex">
|
||||
<Suspense fallback={null}>
|
||||
<CreditsButton />
|
||||
</Suspense>
|
||||
<UpgradeButton />
|
||||
</div>
|
||||
<ColorModeToggle />
|
||||
</div>
|
||||
|
||||
<ClientOnly
|
||||
ssr={
|
||||
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border bg-muted/50">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomConnectWallet />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</header>
|
||||
<HeaderNavLink
|
||||
trackingLabel="chainlist"
|
||||
href="/chainlist"
|
||||
label="Chainlist"
|
||||
/>
|
||||
|
||||
<HeaderNavLink
|
||||
trackingLabel="docs"
|
||||
href="https://portal.thirdweb.com"
|
||||
label="Docs"
|
||||
/>
|
||||
|
||||
<HeaderNavLink
|
||||
trackingLabel="support"
|
||||
href="/support"
|
||||
label="Support"
|
||||
/>
|
||||
|
||||
<div className="md:hidden">
|
||||
<Button asChild variant="ghost">
|
||||
<NavLink
|
||||
className="fade-in-0 !h-auto !w-auto p-2"
|
||||
href="/support"
|
||||
tracking={{
|
||||
category: "header",
|
||||
action: "click",
|
||||
label: "support",
|
||||
}}
|
||||
>
|
||||
<CircleHelpIcon className="size-5" />
|
||||
</NavLink>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ColorModeToggle />
|
||||
|
||||
<div className="md:ml-2">
|
||||
<CustomConnectWallet />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<DashboardHeaderTabs />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function HeaderNavLink(props: {
|
||||
label: string;
|
||||
trackingLabel: string;
|
||||
href: string;
|
||||
}) {
|
||||
return (
|
||||
<NavLink
|
||||
href={props.href}
|
||||
tracking={{
|
||||
category: "header",
|
||||
action: "click",
|
||||
label: props.trackingLabel,
|
||||
}}
|
||||
className="hidden px-2.5 text-muted-foreground text-sm hover:text-foreground md:block"
|
||||
activeClassName="text-foreground"
|
||||
>
|
||||
{props.label}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function DashboardHeaderTabs() {
|
||||
const pathname = usePathname() || "";
|
||||
return (
|
||||
<TabLinks
|
||||
className="w-full"
|
||||
tabContainerClassName="px-4"
|
||||
shadowColor="transparent"
|
||||
links={[
|
||||
{
|
||||
href: "/dashboard",
|
||||
name: "Home",
|
||||
isActive: pathname === "/dashboard",
|
||||
},
|
||||
{
|
||||
href: "/dashboard/connect/analytics",
|
||||
name: "Connect",
|
||||
isActive: pathname.startsWith("/dashboard/connect"),
|
||||
},
|
||||
{
|
||||
href: "/dashboard/contracts/deploy",
|
||||
name: "Contracts",
|
||||
isActive: pathname.startsWith("/dashboard/contracts"),
|
||||
},
|
||||
{
|
||||
href: "/dashboard/engine",
|
||||
name: "Engine",
|
||||
isActive: pathname.startsWith("/dashboard/engine"),
|
||||
},
|
||||
{
|
||||
href: "/dashboard/settings/api-keys",
|
||||
name: "Settings",
|
||||
isActive: pathname.startsWith("/dashboard/settings"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { Menu, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { headerLinks } from "./headerLinks";
|
||||
|
||||
export function MobileBurgerMenu() {
|
||||
const [showBurgerMenu, setShowBurgerMenu] = useState(false);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (showBurgerMenu) {
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
}, [showBurgerMenu]);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
>
|
||||
<Menu strokeWidth={1} />
|
||||
</Button>
|
||||
|
||||
{showBurgerMenu && (
|
||||
<nav className="fade-in-20 slide-in-from-top-3 fixed inset-0 top-[80px] z-50 animate-in border-t bg-background p-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-6 right-6"
|
||||
onClick={() => setShowBurgerMenu(false)}
|
||||
>
|
||||
<XIcon strokeWidth={1} />
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col gap-7">
|
||||
{[...headerLinks.left, ...headerLinks.right].map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-lg "
|
||||
target={link.href.startsWith("https") ? "_blank" : undefined}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { ResourcesDropdownButton } from "./ResourcesDropdownButton";
|
||||
import { AccountButton } from "./account-button.client";
|
||||
|
||||
export function SecondaryNav(props: {
|
||||
email: string | undefined;
|
||||
account: Pick<Account, "email" | "id"> | undefined;
|
||||
logout: () => void;
|
||||
connectButton: React.ReactNode;
|
||||
}) {
|
||||
@@ -27,9 +28,9 @@ export function SecondaryNav(props: {
|
||||
</Link>
|
||||
|
||||
<AccountButton
|
||||
email={props.email}
|
||||
logout={props.logout}
|
||||
connectButton={props.connectButton}
|
||||
account={props.account}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -8,14 +9,15 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { LogOutIcon, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
|
||||
export function AccountButton(props: {
|
||||
email: string | undefined;
|
||||
logout: () => void;
|
||||
connectButton: React.ReactNode;
|
||||
account?: Pick<Account, "email" | "id">;
|
||||
}) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
@@ -27,15 +29,15 @@ export function AccountButton(props: {
|
||||
className="!p-0 !h-auto size-10 rounded-full hover:ring-2 hover:ring-offset-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* TODO - replace with account image */}
|
||||
<div className="size-9 rounded-full border border-border bg-muted" />
|
||||
{/* TODO - set account image */}
|
||||
<GradientAvatar id={props.account?.id} src={""} className="size-9" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[300px] rounded-lg p-0" align="end">
|
||||
<div className="border-border border-b p-4 pb-5">
|
||||
<SkeletonContainer
|
||||
skeletonData="[email protected]"
|
||||
loadedData={props.email}
|
||||
loadedData={props.account?.email}
|
||||
render={(v) => <p className="text-muted-foreground text-sm">{v}</p>}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
export const headerLinks = {
|
||||
left: [
|
||||
{
|
||||
name: "Connect",
|
||||
href: "/dashboard/connect/analytics",
|
||||
},
|
||||
{
|
||||
name: "Contracts",
|
||||
href: "/dashboard/contracts",
|
||||
},
|
||||
{
|
||||
name: "Engine",
|
||||
href: "/dashboard/engine",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/dashboard/settings/api-keys",
|
||||
},
|
||||
],
|
||||
right: [
|
||||
{
|
||||
name: "Chainlist",
|
||||
href: "/chainlist",
|
||||
},
|
||||
{
|
||||
name: "Docs",
|
||||
href: "https://portal.thirdweb.com",
|
||||
},
|
||||
{
|
||||
name: "Support",
|
||||
href: "https://thirdweb.com/support",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link, { type LinkProps } from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type NavLinkProps = Omit<
|
||||
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
keyof LinkProps
|
||||
> &
|
||||
Omit<LinkProps, "href"> & {
|
||||
href: string;
|
||||
activeClassName: string;
|
||||
};
|
||||
|
||||
export const NavLink: React.FC<React.PropsWithChildren<NavLinkProps>> = ({
|
||||
children,
|
||||
className,
|
||||
activeClassName,
|
||||
...props
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const isActive = !!pathname?.startsWith(props.href);
|
||||
return (
|
||||
<Link {...props} className={cn(className, isActive && activeClassName)}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAddress } from "thirdweb";
|
||||
import type {
|
||||
@@ -10,9 +11,6 @@ import type {
|
||||
VerifyLoginPayloadParams,
|
||||
} from "thirdweb/auth";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export async function getLoginPayload(
|
||||
@@ -21,7 +19,7 @@ export async function getLoginPayload(
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/payload`, {
|
||||
const res = await fetch(`${API_SERVER_URL}/v2/siwe/payload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -48,7 +46,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
|
||||
const cookieStore = cookies();
|
||||
|
||||
// forward the request to the API server
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/login`, {
|
||||
const res = await fetch(`${API_SERVER_URL}/v2/siwe/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -144,7 +142,7 @@ export async function isLoggedIn(address: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -78,6 +78,8 @@ function CustomConnectEmmbed() {
|
||||
modalSize={isLG ? "wide" : "compact"}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
className="shadow-lg"
|
||||
privacyPolicyUrl="/privacy"
|
||||
termsOfServiceUrl="/tos"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
|
||||
import { CopyButton } from "@/components/ui/CopyButton";
|
||||
import {
|
||||
Select,
|
||||
@@ -119,8 +120,8 @@ function ProjectCard(props: {
|
||||
key={project.id}
|
||||
className="relative flex items-center gap-4 rounded-lg border border-border bg-muted/50 p-4 transition-colors hover:bg-muted/70"
|
||||
>
|
||||
{/* TODO - replace with project image */}
|
||||
<div className="size-10 shrink-0 rounded-full bg-border" />
|
||||
{/* TODO - set image */}
|
||||
<ProjectAvatar className="size-10 rounded-full" src="" />
|
||||
|
||||
<div>
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import type { Team } from "@/api/team";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { TeamSettingsSidebar } from "./_components/sidebar/TeamSettingsSidebar";
|
||||
import { TeamSettingsMobileNav } from "./_components/sidebar/TeamsMobileNav";
|
||||
import { getTeamSettingsLinks } from "./_components/sidebar/getTeamSettingsLinks";
|
||||
|
||||
// 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;
|
||||
}) {
|
||||
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);
|
||||
const accountQuery = useAccount();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 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
|
||||
teamSlug={props.team.slug}
|
||||
showFull={showFullNavOnMobile}
|
||||
setShowFull={setShowFullNavOnMobile}
|
||||
activeLink={activeLink}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container flex grow gap-8 lg:min-h-[900px] [&>*]:py-8 lg:[&>*]:py-10">
|
||||
<TeamSettingsSidebar team={props.team} account={accountQuery.data} />
|
||||
<div
|
||||
className={cn(
|
||||
"grow",
|
||||
// if showing full nav on mobile - hide the page content
|
||||
showFullNavOnMobile && "max-sm:hidden",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+34
-6
@@ -1,10 +1,14 @@
|
||||
import type { Team } from "@/api/team";
|
||||
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { SidebarLink } from "./SidebarLink";
|
||||
import { getTeamSettingsLinks } from "./getTeamSettingsLinks";
|
||||
|
||||
export function TeamSettingsSidebar(props: {
|
||||
teamSlug: string;
|
||||
team: Team;
|
||||
account: Pick<Account, "id"> | undefined;
|
||||
}) {
|
||||
const teamLinks = getTeamSettingsLinks(props.teamSlug);
|
||||
const teamLinks = getTeamSettingsLinks(props.team.slug);
|
||||
const teamGroupLinks = teamLinks.filter((link) => link.group === "team");
|
||||
const accountGroupLinks = teamLinks.filter(
|
||||
(link) => link.group === "account",
|
||||
@@ -12,8 +16,24 @@ export function TeamSettingsSidebar(props: {
|
||||
|
||||
return (
|
||||
<aside className="lg:-ml-2 sticky top-0 hidden w-[250px] shrink-0 grow-0 flex-col gap-4 self-start lg:flex">
|
||||
<RenderLinkGroup links={teamGroupLinks} title="Team" />
|
||||
<RenderLinkGroup links={accountGroupLinks} title="Account" />
|
||||
<RenderLinkGroup
|
||||
links={teamGroupLinks}
|
||||
title="Team"
|
||||
team={props.team}
|
||||
titleAvatarIcon={{
|
||||
id: props.team.id,
|
||||
src: "", // TODO - set team image
|
||||
}}
|
||||
/>
|
||||
<RenderLinkGroup
|
||||
links={accountGroupLinks}
|
||||
title="Account"
|
||||
team={props.team}
|
||||
titleAvatarIcon={{
|
||||
id: props.account?.id,
|
||||
src: "", // TODO - set team image
|
||||
}}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -24,12 +44,20 @@ function RenderLinkGroup(props: {
|
||||
href: string;
|
||||
}>;
|
||||
title: string;
|
||||
team: Team;
|
||||
titleAvatarIcon: {
|
||||
id: string | undefined;
|
||||
src: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 px-4">
|
||||
{/* TODO - using placeholder for now */}
|
||||
<div className="size-4 rounded-full border border-border bg-muted" />
|
||||
<GradientAvatar
|
||||
src={props.titleAvatarIcon.src}
|
||||
className="size-4"
|
||||
id={props.titleAvatarIcon.id}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">{props.title}</p>
|
||||
</div>
|
||||
|
||||
|
||||
+4
-2
@@ -4,7 +4,9 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Billing } from "components/settings/Account/Billing";
|
||||
|
||||
export const SettingsBillingPage = () => {
|
||||
export const SettingsBillingPage = (props: {
|
||||
teamId: string | undefined;
|
||||
}) => {
|
||||
const meQuery = useAccount({
|
||||
refetchInterval: (query) =>
|
||||
[
|
||||
@@ -25,5 +27,5 @@ export const SettingsBillingPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return <Billing account={account} />;
|
||||
return <Billing account={account} teamId={props.teamId} />;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { notFound } from "next/navigation";
|
||||
import { SettingsBillingPage } from "./BillingSettingsPage";
|
||||
|
||||
export default function Page() {
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
team_slug: string;
|
||||
};
|
||||
}) {
|
||||
const team = await getTeamBySlug(props.params.team_slug);
|
||||
|
||||
if (!team) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SettingsBillingPage />
|
||||
<SettingsBillingPage teamId={team.id} />
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user