Compare commits
133
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fecd3a6c16 | ||
|
|
8aeb4d6308 | ||
|
|
f1cd2539d1 | ||
|
|
a322e16853 | ||
|
|
e50d77d28f | ||
|
|
7f3b91da37 | ||
|
|
41d50443d9 | ||
|
|
a8ac98bd49 | ||
|
|
0781da9dca | ||
|
|
c5dfad8dbb | ||
|
|
9663079062 | ||
|
|
1f6bb7c329 | ||
|
|
30e13e6b91 | ||
|
|
2dfc245d44 | ||
|
|
64d7bf358f | ||
|
|
a6e9c97c6f | ||
|
|
ee57ded902 | ||
|
|
6d3947ab0c | ||
|
|
632f1d138f | ||
|
|
18f0e575c4 | ||
|
|
e732b11e45 | ||
|
|
f77165e2d1 | ||
|
|
dfd64d2896 | ||
|
|
4e703fdb5d | ||
|
|
af88935680 | ||
|
|
c50c82aacd | ||
|
|
0fa8354dec | ||
|
|
769a927627 | ||
|
|
ceff873cba | ||
|
|
9a432af1cf | ||
|
|
ee6bd4f898 | ||
|
|
ccf6329810 | ||
|
|
821caa63d7 | ||
|
|
09e396d856 | ||
|
|
9573b2f9d2 | ||
|
|
b004a07e5d | ||
|
|
2396b4e461 | ||
|
|
ca6c913361 | ||
|
|
cf166e4c04 | ||
|
|
a51192ffbd | ||
|
|
6fd36aabc7 | ||
|
|
08cc48910d | ||
|
|
f03ded300f | ||
|
|
b651bae4ba | ||
|
|
66833a832c | ||
|
|
9d5828eeab | ||
|
|
b191658fcd | ||
|
|
a6b7e8d818 | ||
|
|
f4f93e6353 | ||
|
|
b693b78645 | ||
|
|
daceeed2ec | ||
|
|
5f7c726917 | ||
|
|
2405456e5f | ||
|
|
a76e6534c4 | ||
|
|
0fb586db52 | ||
|
|
f8ebbe54d9 | ||
|
|
ca756819db | ||
|
|
7217e774b2 | ||
|
|
5de72c5ecd | ||
|
|
60c5c26583 | ||
|
|
dc74c3b4b9 | ||
|
|
69a921d01f | ||
|
|
9abdb92028 | ||
|
|
6ed2fd13de | ||
|
|
a22457a69c | ||
|
|
ae675db8c8 | ||
|
|
54c9333fc9 | ||
|
|
541cf0b3e9 | ||
|
|
c65022174d | ||
|
|
6818cbabf2 | ||
|
|
a7fb844d87 | ||
|
|
bf431961ec | ||
|
|
c5d4cab8f8 | ||
|
|
1ee0dffec4 | ||
|
|
33611409b0 | ||
|
|
ab55aec10c | ||
|
|
ca22e4659e | ||
|
|
411895c002 | ||
|
|
cfa1c20a91 | ||
|
|
2b16f90715 | ||
|
|
e8892b152f | ||
|
|
073b25afd7 | ||
|
|
4550bb2663 | ||
|
|
f5f8a40320 | ||
|
|
3569763501 | ||
|
|
1401f8d039 | ||
|
|
1616b7f619 | ||
|
|
aa548a359f | ||
|
|
eef52dfa7e | ||
|
|
19ec571500 | ||
|
|
47dce2e05c | ||
|
|
8b4695224c | ||
|
|
94e2d5b8dd | ||
|
|
40fa323294 | ||
|
|
2cebb420e6 | ||
|
|
e89ddb24f2 | ||
|
|
86f205e2aa | ||
|
|
6c277ae676 | ||
|
|
153651df35 | ||
|
|
8ba460b6fc | ||
|
|
1e61a9edfc | ||
|
|
d03e687aa9 | ||
|
|
e1c0dbe19e | ||
|
|
3e7df081d7 | ||
|
|
b6f189c220 | ||
|
|
fb4d0a26f1 | ||
|
|
bc345371ba | ||
|
|
7a8e4e5f86 | ||
|
|
6ba3b30b60 | ||
|
|
5ad442cf0c | ||
|
|
5756c74a71 | ||
|
|
a1cff7a579 | ||
|
|
eafaefec20 | ||
|
|
b81d7a1854 | ||
|
|
21cc45adc2 | ||
|
|
a67d342779 | ||
|
|
60edce49c0 | ||
|
|
20c0dca958 | ||
|
|
4b30b847e2 | ||
|
|
b2cc17fd1e | ||
|
|
ace1dc5b09 | ||
|
|
9f79277344 | ||
|
|
6e0fd205ff | ||
|
|
69b1ee9176 | ||
|
|
0a19142a24 | ||
|
|
feb4fd0908 | ||
|
|
1d8695eb42 | ||
|
|
858dc53a25 | ||
|
|
8997fac1b0 | ||
|
|
403e4224e4 | ||
|
|
de039ef462 | ||
|
|
35fd3bcafa | ||
|
|
80a22ff45e |
+1
-1
@@ -12,7 +12,7 @@ packages/eslint-config-thirdweb/ @thirdweb-dev/core-platform @edwardysun
|
||||
packages/tw-tsconfig/ @thirdweb-dev/core-platform @edwardysun
|
||||
|
||||
# apps
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform @jakubkrehel
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform
|
||||
|
||||
# .github folder + .changeset + turbo config is owned by jonas for now
|
||||
scripts/ @jnsdls @joaquim-verges
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
---
|
||||
<!--
|
||||
|
||||
## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
|
||||
If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):
|
||||
|
||||
## Notes for the reviewer
|
||||
|
||||
Anything important to call out? Be sure to also clarify these in your comments.
|
||||
|
||||
## How to test
|
||||
|
||||
Unit tests, playground, etc.
|
||||
|
||||
-->
|
||||
|
||||
Vendored
+5
-1
@@ -27,5 +27,9 @@
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
"/connect/account-abstraction",
|
||||
"/connect/universal-bridge",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
|
||||
+42
-42
@@ -31,29 +31,29 @@
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-hover-card": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-hover-card": "^1.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@sentry/nextjs": "8.45.1",
|
||||
"@shazow/whatsabi": "^0.18.0",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.53.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.2",
|
||||
"@vercel/og": "^0.6.4",
|
||||
"abitype": "1.0.7",
|
||||
"@vercel/functions": "^1.6.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -62,19 +62,19 @@
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"framer-motion": "12.0.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -83,17 +83,17 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.14.1",
|
||||
"recharts": "2.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.1",
|
||||
"sonner": "^1.7.4",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.18.2",
|
||||
"swagger-ui-react": "^5.18.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
@@ -103,42 +103,42 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@next/bundle-analyzer": "15.1.0",
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/addon-viewport": "8.4.7",
|
||||
"@storybook/blocks": "8.4.7",
|
||||
"@storybook/nextjs": "8.4.7",
|
||||
"@storybook/react": "8.5.0",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-onboarding": "8.5.2",
|
||||
"@storybook/addon-viewport": "8.5.2",
|
||||
"@storybook/blocks": "8.5.2",
|
||||
"@storybook/nextjs": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.15.0",
|
||||
"checkly": "^4.19.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"knip": "5.41.0",
|
||||
"knip": "5.43.6",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"storybook": "8.4.7",
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +331,18 @@ async function redirects() {
|
||||
destination: "/templates/:slug",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /account-abstraction to /connect/account-abstraction
|
||||
{
|
||||
source: "/account-abstraction",
|
||||
destination: "/connect/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /connect/pay to /connect/universal-bridge
|
||||
{
|
||||
source: "/connect/pay",
|
||||
destination: "/connect/universal-bridge",
|
||||
permanent: false,
|
||||
},
|
||||
// PREVIOUS CAMPAIGNS
|
||||
{
|
||||
source: "/unlimited-wallets",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { API_SERVER_URL } from "../constants/env";
|
||||
export async function updateAccount(values: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
image?: string | null;
|
||||
}) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AnalyticsQueryParams,
|
||||
InAppWalletStats,
|
||||
RpcMethodStats,
|
||||
TransactionStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
@@ -77,7 +78,10 @@ export async function getUserOpUsage(
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch user ops usage");
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch user ops usage: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -85,6 +89,30 @@ export async function getUserOpUsage(
|
||||
return json.data as UserOpStats[];
|
||||
}
|
||||
|
||||
export async function getClientTransactions(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<TransactionStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/transactions/client?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch client transactions stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as TransactionStats[];
|
||||
}
|
||||
|
||||
export async function getRpcMethodUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<RpcMethodStats[]> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -21,11 +20,7 @@ export type Project = {
|
||||
};
|
||||
|
||||
export async function getProjects(teamSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return [];
|
||||
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
|
||||
@@ -25,7 +25,7 @@ export type Team = {
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ type Option = { label: string; value: string };
|
||||
export function MultiNetworkSelector(props: {
|
||||
selectedChainIds: number[];
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -53,16 +55,24 @@ export function MultiNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -79,6 +89,7 @@ export function MultiNetworkSelector(props: {
|
||||
disabled={allChains.length === 0}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -90,6 +101,9 @@ export function SingleNetworkSelector(props: {
|
||||
popoverContentClassName?: string;
|
||||
// if specified - only these chains will be shown
|
||||
chainIds?: number[];
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -140,16 +154,19 @@ export function SingleNetworkSelector(props: {
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
@@ -162,12 +179,15 @@ export function SingleNetworkSelector(props: {
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isLoadingChains}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ interface SelectWithSearchProps
|
||||
) => boolean;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -48,6 +51,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
overrideSearchFn,
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -107,7 +111,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -118,23 +122,24 @@ export const SelectWithSearch = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm",
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("z-[10001] p-0", popoverContentClassName)}
|
||||
align="center"
|
||||
align={props.align || "center"}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
@@ -174,7 +179,12 @@ export const SelectWithSearch = React.forwardRef<
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -148,21 +148,6 @@ export function WalletAddress(props: {
|
||||
);
|
||||
})
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
variant="upsell"
|
||||
className="flex flex-row items-center gap-2 text-sm"
|
||||
size="sm"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://blog.thirdweb.com/changelog/introducing-the-social-sdk?ref=dashboard-social-wallet"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
|
||||
export function CopyTextButton(props: {
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
tooltip: string;
|
||||
tooltip: string | undefined;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
variant?:
|
||||
|
||||
@@ -95,7 +95,8 @@ export const engineKeys = {
|
||||
backendWallet: (address: string, chainId: number) =>
|
||||
["backendWallet", address, chainId] as const,
|
||||
backendWalletBalance: (address: string, chainId: number) =>
|
||||
[...engineKeys.backendWallet(address, chainId), "balance"] as const,
|
||||
["backendWallet", "balance", address, chainId] as const,
|
||||
backendWalletBalanceAll: () => ["backendWallet", "balance"] as const,
|
||||
corsUrls: (instance: string) =>
|
||||
[...engineKeys.all, instance, "corsUrls"] as const,
|
||||
ipAllowlist: (instance: string) =>
|
||||
|
||||
@@ -39,6 +39,7 @@ export type Account = {
|
||||
id: string;
|
||||
isStaff: boolean;
|
||||
creatorWalletAddress: string;
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
email?: string;
|
||||
advancedEnabled: boolean;
|
||||
@@ -548,41 +549,6 @@ export function useResendEmailConfirmation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys(props: {
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryKey: apiKeys.keys(address || ""),
|
||||
queryFn: async () => {
|
||||
type Result = {
|
||||
data: ApiKey[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/keys",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!address && props.isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateApiKey() {
|
||||
const address = useActiveAccount()?.address;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
@@ -469,12 +469,12 @@ export function useEngineBackendWalletBalance(params: {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
const { instanceUrl, address, authToken } = params;
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const { instanceUrl, address, authToken, chainId } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId || 1),
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId),
|
||||
queryFn: async () => {
|
||||
invariant(chainId, "chainId is required");
|
||||
|
||||
|
||||
+225
-21
@@ -1,13 +1,32 @@
|
||||
"use client";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/env";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
@@ -17,9 +36,21 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import {
|
||||
type ThirdwebClient,
|
||||
prepareTransaction,
|
||||
toUnits,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletChain,
|
||||
useSendTransaction,
|
||||
useSwitchActiveWalletChain,
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
@@ -137,6 +168,35 @@ export function FaucetButton({
|
||||
|
||||
const form = useForm<z.infer<typeof claimFaucetSchema>>();
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-3 text-center text-muted-foreground text-sm">
|
||||
Faucet is empty right now
|
||||
</div>
|
||||
<SendFundsToFaucetModalButton
|
||||
chain={definedChain}
|
||||
isLoggedIn={!!twAccount}
|
||||
client={client}
|
||||
chainMeta={chain}
|
||||
onFaucetRefill={() => {
|
||||
faucetWalletBalanceQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Force users to log in to claim the faucet
|
||||
if (!address || !twAccount) {
|
||||
return (
|
||||
@@ -164,24 +224,6 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<Button variant="outline" disabled className="!opacity-100 w-full">
|
||||
Faucet is empty right now
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
@@ -250,3 +292,165 @@ export function FaucetButton({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const faucetFormSchema = z.object({
|
||||
amount: z.coerce.number().refine((value) => value > 0, {
|
||||
message: "Amount must be greater than 0",
|
||||
}),
|
||||
});
|
||||
|
||||
function SendFundsToFaucetModalButton(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default" className="w-full">
|
||||
Refill Faucet
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-2">
|
||||
<DialogTitle>Refill Faucet</DialogTitle>
|
||||
<DialogDescription>Send funds to faucet wallet</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<SendFundsToFaucetModalContent {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SendFundsToFaucetModalContent(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
const switchActiveWalletChain = useSwitchActiveWalletChain();
|
||||
const sendTxMutation = useSendTransaction({
|
||||
payModal: false,
|
||||
});
|
||||
const switchChainMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await switchActiveWalletChain(props.chain);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof faucetFormSchema>>({
|
||||
resolver: zodResolver(faucetFormSchema),
|
||||
defaultValues: {
|
||||
amount: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
|
||||
const sendNativeTokenTx = prepareTransaction({
|
||||
chain: props.chain,
|
||||
client: props.client,
|
||||
to: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
value: toWei(values.amount.toString()),
|
||||
});
|
||||
|
||||
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.onFaucetRefill();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex min-w-0 flex-col gap-5"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
|
||||
<CopyTextButton
|
||||
copyIconPosition="right"
|
||||
variant="outline"
|
||||
className="w-full justify-between bg-card py-2 font-mono"
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
|
||||
{props.chainMeta.nativeCurrency.symbol}
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!account && (
|
||||
<CustomConnectWallet
|
||||
chain={props.chain}
|
||||
loginRequired={false}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
connectButtonClassName="!w-full"
|
||||
detailsButtonClassName="!w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{account && activeChain && (
|
||||
<div>
|
||||
{activeChain.id === props.chain.id ? (
|
||||
<Button
|
||||
key="submit"
|
||||
type="submit"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={sendTxMutation.isPending}
|
||||
>
|
||||
{sendTxMutation.isPending && <Spinner className="size-4" />}
|
||||
Send funds to faucet
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="switch"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={switchChainMutation.isPending}
|
||||
onClick={() => switchChainMutation.mutate()}
|
||||
>
|
||||
Switch to {props.chainMeta.name}{" "}
|
||||
{switchChainMutation.isPending && (
|
||||
<Spinner className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-1
@@ -4,6 +4,7 @@ import {
|
||||
toTokens,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
@@ -71,7 +72,10 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
conditions.map(async (condition) => {
|
||||
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
|
||||
await ERC20Ext.getCurrencyMetadata({
|
||||
contract: { ...options.contract, address: condition.currency },
|
||||
contract: getContract({
|
||||
...options.contract,
|
||||
address: condition.currency,
|
||||
}),
|
||||
}).then((m) => ({
|
||||
...m,
|
||||
displayValue: toTokens(condition.pricePerToken, m.decimals),
|
||||
|
||||
-6
@@ -39,12 +39,6 @@ export function getContractPageSidebarLinks(data: {
|
||||
href: `${layoutPrefix}/events`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Embed",
|
||||
href: `${layoutPrefix}/embed`,
|
||||
hide: data.metadata.embedType === null,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
href: `${layoutPrefix}/analytics`,
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export function EmbedSetupClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.data.embedType === null) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={props.contract}
|
||||
ercOrMarketplace={metadataQuery.data.embedType}
|
||||
twAccount={props.twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-612
@@ -1,612 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import {
|
||||
type Account,
|
||||
useApiKeys,
|
||||
useCreateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
|
||||
|
||||
interface EmbedSetupProps {
|
||||
contract: ThirdwebContract;
|
||||
ercOrMarketplace: string;
|
||||
twAccount: Account | undefined;
|
||||
}
|
||||
|
||||
// MAKE SURE THIS IS v1 embed hashes!!
|
||||
const IPFS_URI = "bafybeigdie2yyiazou7grjowoevmuip6akk33nqb55vrpezqdwfssrxyfy";
|
||||
const ERC721_IPFS_URI =
|
||||
"bafybeicd3qfzelz4su7ng6n523virdsgobrc5pcbarhwqv3dj3drh645pi";
|
||||
|
||||
interface IframeSrcOptions {
|
||||
clientId: string;
|
||||
chain: string;
|
||||
tokenId?: string;
|
||||
listingId?: string;
|
||||
listingType?: string;
|
||||
directListingId?: string;
|
||||
englishAuctionId?: string;
|
||||
relayUrl?: string;
|
||||
theme?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
biconomyApiKey?: string;
|
||||
biconomyApiId?: string;
|
||||
}
|
||||
|
||||
const colorOptions = [
|
||||
"purple",
|
||||
"blue",
|
||||
"orange",
|
||||
"pink",
|
||||
"green",
|
||||
"red",
|
||||
"teal",
|
||||
"cyan",
|
||||
"yellow",
|
||||
];
|
||||
|
||||
const isValidUrl = (url: string | undefined) => {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const buildIframeSrc = (
|
||||
contract: ThirdwebContract,
|
||||
ercOrMarketplace?: string,
|
||||
options?: IframeSrcOptions,
|
||||
): string => {
|
||||
const contractPath =
|
||||
ercOrMarketplace === "erc721" ? "" : `${ercOrMarketplace}.html`;
|
||||
const contractEmbedHash =
|
||||
ercOrMarketplace === "erc721" ? ERC721_IPFS_URI : IPFS_URI;
|
||||
|
||||
if (!contract || !options || !contractEmbedHash) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const {
|
||||
clientId,
|
||||
chain,
|
||||
tokenId,
|
||||
listingId,
|
||||
listingType,
|
||||
directListingId,
|
||||
englishAuctionId,
|
||||
relayUrl,
|
||||
theme,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
biconomyApiKey,
|
||||
biconomyApiId,
|
||||
} = options;
|
||||
|
||||
const url = new URL(
|
||||
`https://embed.ipfscdn.io/ipfs/${contractEmbedHash}/${contractPath}`,
|
||||
);
|
||||
|
||||
url.searchParams.append("contract", contract.address);
|
||||
url.searchParams.append("chain", chain);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
|
||||
if (tokenId !== undefined && ercOrMarketplace === "erc1155") {
|
||||
url.searchParams.append("tokenId", tokenId.toString());
|
||||
}
|
||||
if (listingId !== undefined && ercOrMarketplace === "marketplace") {
|
||||
url.searchParams.append("listingId", listingId.toString());
|
||||
}
|
||||
if (
|
||||
directListingId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "direct-listing"
|
||||
) {
|
||||
url.searchParams.append("directListingId", directListingId.toString());
|
||||
}
|
||||
if (
|
||||
englishAuctionId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "english-auction"
|
||||
) {
|
||||
url.searchParams.append("englishAuctionId", englishAuctionId.toString());
|
||||
}
|
||||
if (isValidUrl(relayUrl)) {
|
||||
url.searchParams.append("relayUrl", relayUrl || "");
|
||||
}
|
||||
if (biconomyApiKey) {
|
||||
url.searchParams.append("biconomyApiKey", biconomyApiKey);
|
||||
}
|
||||
if (biconomyApiId) {
|
||||
url.searchParams.append("biconomyApiId", biconomyApiId);
|
||||
}
|
||||
if (theme) {
|
||||
url.searchParams.append("theme", theme);
|
||||
}
|
||||
if (primaryColor && primaryColor !== "default") {
|
||||
url.searchParams.append("primaryColor", primaryColor);
|
||||
}
|
||||
if (secondaryColor && secondaryColor !== "orange") {
|
||||
url.searchParams.append("secondaryColor", secondaryColor);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
function minimizeChain(
|
||||
chain: ChainMetadata,
|
||||
): Pick<
|
||||
ChainMetadata,
|
||||
| "name"
|
||||
| "chain"
|
||||
| "rpc"
|
||||
| "nativeCurrency"
|
||||
| "shortName"
|
||||
| "chainId"
|
||||
| "testnet"
|
||||
| "slug"
|
||||
| "icon"
|
||||
> {
|
||||
const [firstRpc] = chain.rpc;
|
||||
return {
|
||||
name: chain.name,
|
||||
chain: chain.chain,
|
||||
rpc: [firstRpc || ""],
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
shortName: chain.shortName,
|
||||
chainId: chain.chainId,
|
||||
testnet: chain.testnet,
|
||||
slug: chain.slug,
|
||||
icon: chain.icon,
|
||||
};
|
||||
}
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
type ChainConfiguration = {
|
||||
rpc?: string | string[];
|
||||
};
|
||||
|
||||
function configureChain(
|
||||
chain: ChainMetadata,
|
||||
chainConfig: ChainConfiguration,
|
||||
): ChainMetadata {
|
||||
let additionalRPCs: string[] = [];
|
||||
if (chainConfig?.rpc) {
|
||||
if (typeof chainConfig.rpc === "string") {
|
||||
additionalRPCs = [chainConfig.rpc];
|
||||
} else {
|
||||
additionalRPCs = chainConfig.rpc;
|
||||
}
|
||||
}
|
||||
// prepend additional RPCs to the chain's RPCs
|
||||
return { ...chain, rpc: [...additionalRPCs, ...chain.rpc] };
|
||||
}
|
||||
|
||||
export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
contract,
|
||||
ercOrMarketplace,
|
||||
twAccount,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const apiKeys = useApiKeys({
|
||||
isLoggedIn: !!twAccount,
|
||||
});
|
||||
const createKeyMutation = useCreateApiKey();
|
||||
|
||||
const validApiKey = (apiKeys.data || []).find(
|
||||
(apiKey) =>
|
||||
(apiKey.domains.includes("*") ||
|
||||
apiKey.domains.includes("embed.ipfscdn.io") ||
|
||||
apiKey.domains.includes("*.ipfscdn.io")) &&
|
||||
(apiKey.services || [])
|
||||
.find((service) => service.name === "storage")
|
||||
?.actions.includes("read") &&
|
||||
!!(apiKey.services || []).find((service) => service.name === "rpc"),
|
||||
);
|
||||
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
|
||||
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
|
||||
name: "Unknown Chain",
|
||||
chainId: chainId || -1,
|
||||
rpc: [],
|
||||
chain: "unknown",
|
||||
nativeCurrency: {
|
||||
name: "Ethereum",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
},
|
||||
shortName: "unknown",
|
||||
slug: "unknown",
|
||||
testnet: false,
|
||||
stackType: "",
|
||||
};
|
||||
|
||||
const { register, watch } = useForm<{
|
||||
rpcUrl: string;
|
||||
relayUrl: string;
|
||||
tokenId: string;
|
||||
listingId: string;
|
||||
listingType: string;
|
||||
directListingId: string;
|
||||
englishAuctionId: string;
|
||||
theme: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
biconomyApiKey: string;
|
||||
biconomyApiId: string;
|
||||
gasless: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
rpcUrl: chain?.rpc[0],
|
||||
tokenId: "0",
|
||||
listingId: "0",
|
||||
directListingId: "0",
|
||||
englishAuctionId: "0",
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
primaryColor: "purple",
|
||||
secondaryColor: "orange",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const configuredChainWithNewRpc = configureChain(chain, {
|
||||
rpc: watch("rpcUrl"),
|
||||
});
|
||||
const minimizedChain = minimizeChain(configuredChainWithNewRpc);
|
||||
|
||||
const iframeSrc = buildIframeSrc(contract, ercOrMarketplace, {
|
||||
chain: JSON.stringify(minimizedChain),
|
||||
clientId: validApiKey?.key || "",
|
||||
...watch(),
|
||||
});
|
||||
|
||||
const embedCode = useMemo(
|
||||
() =>
|
||||
`<iframe
|
||||
src="${iframeSrc}"
|
||||
width="100%"
|
||||
height="750px"
|
||||
style="max-width:100%;"
|
||||
frameborder="0"
|
||||
></iframe>`,
|
||||
[iframeSrc],
|
||||
);
|
||||
|
||||
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
|
||||
const activeAccount = useActiveAccount();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LazyCreateAPIKeyDialog
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
domains: "embed.ipfscdn.io",
|
||||
}}
|
||||
open={showCreateAPIKeyModal}
|
||||
onOpenChange={setShowCreateAPIKeyModal}
|
||||
onCreateAndComplete={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "success",
|
||||
fromEmbed: true,
|
||||
});
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
/>
|
||||
|
||||
<Alert variant="warning">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Deprecated</AlertTitle>
|
||||
<AlertDescription className="leading-relaxed">
|
||||
<span>
|
||||
thirdweb NFT Embeds are deprecated and not actively maintained
|
||||
</span>{" "}
|
||||
<br />
|
||||
<span>
|
||||
Use the{" "}
|
||||
<Link
|
||||
href="https://github.com/thirdweb-example/nft-minting-template"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{" "}
|
||||
NFT Minting Template
|
||||
</Link>{" "}
|
||||
or build a custom NFT minting app using{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/react/v5"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
thirdweb React SDK components
|
||||
</Link>{" "}
|
||||
instead
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<Card className="flex w-full flex-col gap-5 bg-card">
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
<Input type="number" {...register("listingId")} />
|
||||
<FormHelperText>
|
||||
The listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
<Select {...register("listingType")}>
|
||||
<option value="direct-listing">Direct Listing</option>
|
||||
<option value="english-auction">English Auction</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The type of listing the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Direct Listing ID</FormLabel>
|
||||
<Input type="number" {...register("directListingId")} />
|
||||
<FormHelperText>
|
||||
The direct listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
<FormLabel>English Auction ID</FormLabel>
|
||||
<Input type="number" {...register("englishAuctionId")} />
|
||||
<FormHelperText>
|
||||
The english auction ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<Input type="number" {...register("tokenId")} />
|
||||
<FormHelperText>
|
||||
The token ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={`${validApiKey?.name} - ${validApiKey?.key}`}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "attempt",
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
Create Client ID
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormHelperText>
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
<FormHelperText>
|
||||
RPC the embed should use to connect to the blockchain.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? null : (
|
||||
<FormControl gap={4}>
|
||||
<Heading size="title.sm" mb={2}>
|
||||
Gasless
|
||||
</Heading>
|
||||
<Select {...register("gasless")} mb={4}>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="openZeppelin">OpenZeppelin Relayer</option>
|
||||
<option value="biconomy">Biconomy Relayer</option>
|
||||
</Select>
|
||||
{watch("gasless") === "openZeppelin" && (
|
||||
<FormControl>
|
||||
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
|
||||
<Input type="url" {...register("relayUrl")} />
|
||||
</FormControl>
|
||||
)}
|
||||
{watch("gasless") === "biconomy" && (
|
||||
<Flex gap={4} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API key</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiKey")} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API ID</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiId")} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
)}
|
||||
<FormHelperText>
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<Select {...register("theme")}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">User system</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Selecting system will make it so the embed would change
|
||||
depending on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
{ercOrMarketplace === "erc721" && (
|
||||
<option value="default">Default</option>
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Secondary Color</FormLabel>
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Use for secondary actions (like when the user is connected to
|
||||
the wrong network)
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Embed Code
|
||||
</h2>
|
||||
<CodeClient
|
||||
code={embedCode}
|
||||
lang="html"
|
||||
onCopy={() => {
|
||||
trackEvent({
|
||||
category: "embed",
|
||||
action: "click",
|
||||
label: "copy-code",
|
||||
address: contract.address,
|
||||
chainId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
{!validApiKey ? (
|
||||
<Text>You need to create a client ID to use embeds</Text>
|
||||
) : iframeSrc ? (
|
||||
<iframe
|
||||
title="thirdweb embed"
|
||||
src={iframeSrc}
|
||||
width="100%"
|
||||
height="750px"
|
||||
frameBorder="0"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { EmbedSetupClient } from "./EmbedSetup.client";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <EmbedSetupClient contract={contract} twAccount={account} />;
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(contract);
|
||||
|
||||
if (embedType === null) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={contract}
|
||||
ercOrMarketplace={embedType}
|
||||
twAccount={account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+33
-14
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { addDays, fromUnixTime } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
claimConditionQuery.data?.startTimestamp === 0 &&
|
||||
claimConditionQuery.data?.endTimestamp === 0;
|
||||
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionQuery.data?.currency || "",
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
const claimConditionCurrency = claimConditionQuery.data?.currency;
|
||||
|
||||
const shouldFetchTokenDecimals =
|
||||
claimConditionQuery.data &&
|
||||
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
|
||||
const shouldFetchCurrencyDecimals =
|
||||
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
|
||||
|
||||
const currencyDecimalsQuery = useReadContract(decimals, {
|
||||
contract: currencyContract,
|
||||
queryOptions: {
|
||||
enabled: shouldFetchTokenDecimals,
|
||||
const currencyDecimalsQuery = useQuery({
|
||||
queryKey: [
|
||||
"currency-decimals",
|
||||
{
|
||||
contractAddress: props.contract.address,
|
||||
chainId: props.contract.chain.id,
|
||||
claimConditionCurrency,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!claimConditionCurrency) {
|
||||
throw new Error();
|
||||
}
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionCurrency,
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
|
||||
const decimalsVal = await decimals({
|
||||
contract: currencyContract,
|
||||
});
|
||||
|
||||
return decimalsVal;
|
||||
},
|
||||
enabled: shouldFetchCurrencyDecimals,
|
||||
});
|
||||
|
||||
const tokenDecimalsQuery = useReadContract(decimals, {
|
||||
contract: contract,
|
||||
queryOptions: {
|
||||
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
// claim conditions data is present
|
||||
claimConditionQuery.data &&
|
||||
// token decimals is fetched if it should be fetched
|
||||
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
? {
|
||||
claimCondition: claimConditionQuery.data,
|
||||
currencyDecimals: currencyDecimalsQuery.data,
|
||||
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
tokenId,
|
||||
isLoading:
|
||||
claimConditionQuery.isLoading ||
|
||||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
|
||||
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
name={props.contractInfo.name}
|
||||
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
|
||||
onClick={() => setAddClaimConditionButtonClicked(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!props.isOwnerAccount}
|
||||
>
|
||||
Add Claim Condition
|
||||
</Button>
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import type { ContractOptions } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleCard } from "./module-card";
|
||||
import { useAllModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
export const InstalledModulesTable = (props: {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
installedModules: {
|
||||
data?: string[];
|
||||
isPending: boolean;
|
||||
|
||||
+10
-7
@@ -15,8 +15,9 @@ import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
type ContractOptions,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
@@ -47,7 +48,7 @@ type FormData = {
|
||||
};
|
||||
|
||||
type InstallModuleFormProps = {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
refetchModules: () => void;
|
||||
account: Account;
|
||||
installedModules: {
|
||||
@@ -180,11 +181,13 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
|
||||
return Promise.all(
|
||||
moduleAddress.map(async (address) => {
|
||||
const result = await resolveImplementation({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
});
|
||||
const result = await resolveImplementation(
|
||||
getContract({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Failed to fetch bytecode for module");
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ import { Suspense, useEffect, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
@@ -35,7 +35,7 @@ import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
type ModuleCardProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount: Account | undefined;
|
||||
allModuleContractInfo: {
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ export async function ChainsData(props: {
|
||||
) : props.activeView === "table" ? (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHeader className="z-0">
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Chain ID</TableHead>
|
||||
|
||||
+10
-29
@@ -83,14 +83,14 @@ export async function ChainListRow({
|
||||
|
||||
<TableCell>
|
||||
<div className="flex w-[520px] flex-row items-center gap-14 ">
|
||||
<div className="z-10 flex items-center gap-7">
|
||||
<div className="z-10 flex items-center gap-4">
|
||||
{products.map((p) => {
|
||||
return (
|
||||
<ProductIcon
|
||||
key={p.name}
|
||||
icon={p.icon}
|
||||
label={p.name}
|
||||
href={pidToHref(p.id)}
|
||||
href={p.link}
|
||||
isEnabled={enabledServices.includes(p.id)}
|
||||
/>
|
||||
);
|
||||
@@ -102,32 +102,6 @@ export async function ChainListRow({
|
||||
);
|
||||
}
|
||||
|
||||
function pidToHref(pid: (typeof products)[number]["id"]) {
|
||||
switch (pid) {
|
||||
case "account-abstraction": {
|
||||
return "https://portal.thirdweb.com/connect/account-abstraction/overview";
|
||||
}
|
||||
case "contracts": {
|
||||
return "https://portal.thirdweb.com/contracts";
|
||||
}
|
||||
case "connect-sdk": {
|
||||
return "https://portal.thirdweb.com/connect";
|
||||
}
|
||||
case "engine": {
|
||||
return "https://portal.thirdweb.com/engine";
|
||||
}
|
||||
case "pay": {
|
||||
return "https://portal.thirdweb.com/connect/pay/overview";
|
||||
}
|
||||
case "rpc-edge": {
|
||||
return "https://portal.thirdweb.com/infrastructure/rpc-edge/overview";
|
||||
}
|
||||
case "insight": {
|
||||
return "https://portal.thirdweb.com/insight";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ProductIcon(props: {
|
||||
icon: React.FC<{ className?: string }>;
|
||||
label: string;
|
||||
@@ -148,8 +122,15 @@ function ProductIcon(props: {
|
||||
<Link
|
||||
href={props.href}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
className="group rounded-lg p-2 hover:bg-accent"
|
||||
>
|
||||
<props.icon className={cn("size-8", !props.isEnabled && "grayscale")} />
|
||||
<props.icon
|
||||
className={cn(
|
||||
"size-6 text-foreground group-hover:text-link-foreground",
|
||||
!props.isEnabled &&
|
||||
"text-muted-foreground opacity-50 group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
|
||||
@@ -106,9 +106,8 @@ export function StarButton(props: {
|
||||
) : (
|
||||
<Star
|
||||
className={cn(
|
||||
"size-6 transition-all",
|
||||
"size-6 text-foreground transition-all",
|
||||
props.iconClassName,
|
||||
isPreferred ? "text-yellow-400" : "text-foreground",
|
||||
)}
|
||||
fill={isPreferred ? "currentColor" : "none"}
|
||||
strokeWidth="1px"
|
||||
|
||||
+4
-74
@@ -7,85 +7,15 @@ export function ConnectSDKIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Connect SDK</title>
|
||||
<rect
|
||||
x="3"
|
||||
y="17.9988"
|
||||
width="14.9988"
|
||||
height="10.6499"
|
||||
rx="2"
|
||||
transform="rotate(-90 3 17.9988)"
|
||||
fill="url(#paint0_linear_195_4047)"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_195_4047)">
|
||||
<path
|
||||
d="M3 9C3 7.89543 3.89543 7 5 7H19C20.1046 7 21 7.89543 21 9V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V9Z"
|
||||
fill="#B9DDFF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M18.3004 12.6995V12.6995C17.5822 13.4177 17.5822 14.5822 18.3004 15.3004V15.3004"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_195_4047"
|
||||
x="1"
|
||||
y="5"
|
||||
width="22"
|
||||
height="18"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_195_4047"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_195_4047"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_195_4047"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_4047"
|
||||
x1="15.026"
|
||||
y1="22.4602"
|
||||
x2="3.73188"
|
||||
y2="27.2831"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2567FF" />
|
||||
<stop offset="1" stopColor="#22A7FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-86
@@ -1,98 +1,21 @@
|
||||
export function ContractIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Contract</title>
|
||||
<g>
|
||||
<rect
|
||||
x="3"
|
||||
y="21"
|
||||
width="14.9988"
|
||||
height="10.6499"
|
||||
rx="1.5"
|
||||
transform="rotate(-90 3 21)"
|
||||
fill="url(#paint0_linear_281_45147)"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_281_45147)">
|
||||
<path
|
||||
d="M7.39429 4.5C7.39429 3.67157 8.06586 3 8.89429 3H19.5C20.3285 3 21 3.67157 21 4.5V19.5C21 20.3284 20.3285 21 19.5 21H8.89429C8.06586 21 7.39429 20.3284 7.39429 19.5V4.5Z"
|
||||
fill="#EBA4D2"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M11.1971 7L17.1971 7.0001"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.1971 10L14.1971 10.0001"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_281_45147"
|
||||
x="5.39429"
|
||||
y="1"
|
||||
width="17.6057"
|
||||
height="22"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_281_45147"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_281_45147"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_281_45147"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_281_45147"
|
||||
x1="-2.77663"
|
||||
y1="30.016"
|
||||
x2="10.9305"
|
||||
y2="13.6496"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F4009F" />
|
||||
<stop offset="1" stopColor="#F856C8" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,77 +7,15 @@ export function EngineIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Engine</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.9523 11.9988C14.9523 11.9981 14.9518 11.9975 14.951 11.9975C14.9089 11.9992 14.8665 12 14.8239 12C13.0588 12 11.6278 10.5691 11.6278 8.8039C11.6278 7.03873 13.0588 5.60779 14.8239 5.60779C15.9762 5.60779 16.986 6.21756 17.5484 7.13202C17.695 7.37046 17.9908 7.48805 18.2658 7.43583C18.4057 7.40926 18.5501 7.39536 18.6978 7.39536C19.9694 7.39536 21.0001 8.42614 21.0001 9.69767C21.0001 10.9692 19.9694 12 18.6979 12C18.6979 12 18.6979 12 18.6979 12C18.6979 12 18.6979 12 18.6978 12H14.9536C14.9529 12 14.9523 11.9994 14.9523 11.9988Z"
|
||||
fill="url(#paint0_linear_195_3943)"
|
||||
d="M6.5 19C4.01472 19 2 16.9853 2 14.5C2 12.1564 3.79151 10.2313 6.07974 10.0194C6.54781 7.17213 9.02024 5 12 5C14.9798 5 17.4522 7.17213 17.9203 10.0194C20.2085 10.2313 22 12.1564 22 14.5C22 16.9853 19.9853 19 17.5 19C13.1102 19 10.3433 19 6.5 19Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_195_3943)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.18764 18.9251C6.17601 18.9253 6.16437 18.9253 6.15271 18.9253C4.41138 18.9253 2.99976 17.5137 2.99976 15.7724C2.99976 14.031 4.41138 12.6194 6.15271 12.6194C6.18301 12.6194 6.20685 12.5932 6.20379 12.563C6.18785 12.4057 6.1797 12.2463 6.1797 12.085C6.1797 9.37046 8.48997 7.16992 11.3398 7.16992C13.8545 7.16992 15.949 8.88324 16.407 11.1512C16.4682 11.4541 16.7615 11.6671 17.0697 11.6434C17.1634 11.6361 17.2581 11.6325 17.3536 11.6325C19.3675 11.6325 21 13.265 21 15.2789C21 17.2928 19.3675 18.9253 17.3536 18.9253C17.285 18.9253 17.2169 18.9234 17.1493 18.9197C17.1063 18.9234 17.0628 18.9253 17.0188 18.9253H6.20985C6.20243 18.9253 6.19503 18.9252 6.18764 18.9251Z"
|
||||
fill="#BFB5FF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_195_3943"
|
||||
x="0.999756"
|
||||
y="5.16992"
|
||||
width="22.0003"
|
||||
height="15.7554"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_195_3943"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_195_3943"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_195_3943"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_3943"
|
||||
x1="13.1976"
|
||||
y1="10.8463"
|
||||
x2="28.7967"
|
||||
y2="2.94276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3F2DAF" />
|
||||
<stop offset="1" stopColor="#917FFB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,47 +7,15 @@ export function InsightIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Insight</title>
|
||||
<circle
|
||||
cx="8.11963"
|
||||
cy="12.9889"
|
||||
r="4.505"
|
||||
fill="url(#paint0_linear_1095_479)"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.2163 7.97235C21.2163 7.96914 21.2164 7.96592 21.2164 7.96271C21.2164 5.60751 18.4863 3.69824 15.1186 3.69824C11.7509 3.69824 9.02089 5.60751 9.02089 7.96271C9.02089 7.96324 9.02089 7.96378 9.02089 7.96431H9.02087V17.0369C9.02087 17.0372 9.02087 17.0374 9.02087 17.0376C9.02137 19.3925 11.7512 21.3014 15.1186 21.3014C18.486 21.3014 21.2158 19.3925 21.2163 17.0376C21.2163 17.0374 21.2163 17.0372 21.2163 17.0369V7.97235Z"
|
||||
fill="#06B6D4"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 10.3516V10.3516C13.5336 11.9944 16.704 11.9944 19.1145 10.3515V10.3515"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
d="M20.5 7.27783L12 12.0001M12 12.0001L3.49997 7.27783M12 12.0001L12 21.5001M21 12.0001V7.94153C21 7.59889 21 7.42757 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7986 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37368 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42757 3 7.59889 3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37368 17.2077 3.52346 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7986 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L13 21.4445M7.5 4.50008L16.5 9.50008M22 21.5001L21 20.5001M22 18.0001C22 19.6569 20.6569 21.0001 19 21.0001C17.3431 21.0001 16 19.6569 16 18.0001C16 16.3432 17.3431 15.0001 19 15.0001C20.6569 15.0001 22 16.3432 22 18.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 15.0107V15.0107C13.5336 16.6536 16.704 16.6535 19.1145 15.0107V15.0107"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1095_479"
|
||||
x1="8.11963"
|
||||
y1="16.3861"
|
||||
x2="-1.14166"
|
||||
y2="3.67577"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#155E75" />
|
||||
<stop offset="1" stopColor="#27B0DB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,79 +7,15 @@ export function PayIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Pay</title>
|
||||
<circle cx="15.9849" cy="16" r="5" fill="url(#paint0_linear_585_2)" />
|
||||
<g filter="url(#filter0_bi_585_2)">
|
||||
<path
|
||||
d="M3 4.5C3 3.67157 3.67157 3 4.5 3H17.5C18.3284 3 19 3.67157 19 4.5V13.5C19 14.3284 18.3284 15 17.5 15H4.5C3.67157 15 3 14.3284 3 13.5V4.5Z"
|
||||
fill="#B9DDFF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="4.98955"
|
||||
y="9.44861"
|
||||
width="3.54886"
|
||||
height="3.54886"
|
||||
rx="1"
|
||||
fill="white"
|
||||
<path
|
||||
d="M22 10H2M2 8.2L2 15.8C2 16.9201 2 17.4802 2.21799 17.908C2.40973 18.2843 2.71569 18.5903 3.09202 18.782C3.51984 19 4.07989 19 5.2 19L18.8 19C19.9201 19 20.4802 19 20.908 18.782C21.2843 18.5903 21.5903 18.2843 21.782 17.908C22 17.4802 22 16.9201 22 15.8V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.7157 21.2843 5.40974 20.908 5.21799C20.4802 5 19.9201 5 18.8 5L5.2 5C4.0799 5 3.51984 5 3.09202 5.21799C2.7157 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.07989 2 8.2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M3.00316 5.99185H19" stroke="white" />
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_585_2"
|
||||
x="1"
|
||||
y="1"
|
||||
width="20"
|
||||
height="16"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_585_2"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_585_2"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_585_2"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_585_2"
|
||||
x1="19.003"
|
||||
y1="15.1892"
|
||||
x2="10.8494"
|
||||
y2="17.6614"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2567FF" />
|
||||
<stop offset="1" stopColor="#22A7FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,92 +7,15 @@ export function RPCIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>RPC</title>
|
||||
<rect
|
||||
x="12.0015"
|
||||
y="12.5007"
|
||||
width="8.99854"
|
||||
height="8.99854"
|
||||
rx="1.5"
|
||||
fill="url(#paint0_linear_195_3994)"
|
||||
/>
|
||||
<g filter="url(#filter0_i_195_3994)">
|
||||
<path
|
||||
d="M5 18C3.89543 18 3 17.1046 3 16L3 5C3 3.89543 3.89543 3 5 3L16 3C17.1046 3 18 3.89543 18 5L18 16C18 17.1046 17.1046 18 16 18L5 18Z"
|
||||
fill="#BFB5FF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M6.10608 9.20007C7.27946 8.04993 8.85702 7.4057 10.5001 7.4057C12.1431 7.4057 13.7207 8.04993 14.8941 9.20007"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.30289 11.3971C8.88958 10.822 9.67836 10.4999 10.4999 10.4999C11.3214 10.4999 12.1102 10.822 12.6969 11.3971"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.5 13.5941H10.5058"
|
||||
stroke="white"
|
||||
d="M4 11C6.38695 11 8.67613 11.9482 10.364 13.636C12.0518 15.3239 13 17.6131 13 20M4 4C8.24346 4 12.3131 5.68571 15.3137 8.68629C18.3143 11.6869 20 15.7565 20 20M6 19C6 19.5523 5.55228 20 5 20C4.44772 20 4 19.5523 4 19C4 18.4477 4.44772 18 5 18C5.55228 18 6 18.4477 6 19Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_i_195_3994"
|
||||
x="3"
|
||||
y="3"
|
||||
width="15"
|
||||
height="15.2764"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.276368" />
|
||||
<feGaussianBlur stdDeviation="0.138184" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_195_3994"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_3994"
|
||||
x1="13.5087"
|
||||
y1="19.8752"
|
||||
x2="30.3227"
|
||||
y2="14.0648"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3F2DAF" />
|
||||
<stop offset="1" stopColor="#917FFB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-77
@@ -7,86 +7,15 @@ export function SmartAccountIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Smart Account</title>
|
||||
<rect
|
||||
x="11.6693"
|
||||
y="16.9566"
|
||||
width="13.4583"
|
||||
height="9.33069"
|
||||
rx="1.49536"
|
||||
transform="rotate(-90 11.6693 16.9566)"
|
||||
fill="url(#paint0_linear_195_3925)"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_195_3925)">
|
||||
<path
|
||||
d="M8.18321 21.2503C8.7189 21.581 9.37914 21.581 9.91484 21.2503C11.5871 20.2182 15.098 17.6396 15.098 14.1832V10.2763C15.098 9.44263 14.5809 8.69639 13.8003 8.40366L9.75127 6.88528C9.2985 6.71549 8.79955 6.71549 8.34678 6.88528L4.29775 8.40366C3.51715 8.69639 3 9.44263 3 10.2763V14.1832C3 17.6396 6.51095 20.2182 8.18321 21.2503Z"
|
||||
fill="#B9DDFF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M9.04903 14.4024C9.98975 14.4024 10.7524 13.6398 10.7524 12.6991C10.7524 11.7583 9.98975 10.9957 9.04903 10.9957C8.10831 10.9957 7.3457 11.7583 7.3457 12.6991C7.3457 13.6398 8.10831 14.4024 9.04903 14.4024Z"
|
||||
fill="white"
|
||||
d="M13 7L11.8845 4.76892C11.5634 4.1268 11.4029 3.80573 11.1634 3.57116C10.9516 3.36373 10.6963 3.20597 10.4161 3.10931C10.0992 3 9.74021 3 9.02229 3H5.2C4.0799 3 3.51984 3 3.09202 3.21799C2.71569 3.40973 2.40973 3.71569 2.21799 4.09202C2 4.51984 2 5.0799 2 6.2V7M2 7H17.2C18.8802 7 19.7202 7 20.362 7.32698C20.9265 7.6146 21.3854 8.07354 21.673 8.63803C22 9.27976 22 10.1198 22 11.8V16.2C22 17.8802 22 18.7202 21.673 19.362C21.3854 19.9265 20.9265 20.3854 20.362 20.673C19.7202 21 18.8802 21 17.2 21H6.8C5.11984 21 4.27976 21 3.63803 20.673C3.07354 20.3854 2.6146 19.9265 2.32698 19.362C2 18.7202 2 17.8802 2 16.2V7ZM12 17.5C12 17.5 15 16.0701 15 13.9252V11.4229L12.8124 10.6412C12.2868 10.4529 11.712 10.4529 11.1864 10.6412L9 11.4229V13.9252C9 16.0701 12 17.5 12 17.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.04906 13.6652C9.3792 13.6652 9.6472 13.9415 9.6472 14.2828V16.7528C9.6472 17.0937 9.37954 17.3704 9.04906 17.3704C8.71893 17.3704 8.45093 17.094 8.45093 16.7528V14.2828C8.45093 13.9419 8.71859 13.6652 9.04906 13.6652Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_195_3925"
|
||||
x="1"
|
||||
y="4.75793"
|
||||
width="16.098"
|
||||
height="18.7404"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_195_3925"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_195_3925"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_195_3925"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_3925"
|
||||
x1="22.4602"
|
||||
y1="20.8654"
|
||||
x2="12.4019"
|
||||
y2="25.2643"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2567FF" />
|
||||
<stop offset="1" stopColor="#22A7FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NebulaIcon } from "../../../../nebula-app/(app)/icons/NebulaIcon";
|
||||
import type { ChainSupportedService } from "../../types/chain";
|
||||
import { ConnectSDKIcon } from "./icons/ConnectSDKIcon";
|
||||
import { ContractIcon } from "./icons/ContractIcon";
|
||||
@@ -13,14 +14,14 @@ export const products = [
|
||||
id: "contracts",
|
||||
icon: ContractIcon,
|
||||
description: "Pre-built contracts & deployment tools",
|
||||
link: "https://portal.thirdweb.com/contracts",
|
||||
link: "https://thirdweb.com/contracts",
|
||||
},
|
||||
{
|
||||
name: "Connect SDK",
|
||||
id: "connect-sdk",
|
||||
icon: ConnectSDKIcon,
|
||||
description: "Client-side SDKs for wallets and blockchain interactions",
|
||||
link: "https://portal.thirdweb.com/connect",
|
||||
link: "https://thirdweb.com/connect",
|
||||
},
|
||||
{
|
||||
name: "RPC Edge",
|
||||
@@ -34,7 +35,7 @@ export const products = [
|
||||
id: "engine",
|
||||
icon: EngineIcon,
|
||||
description: "Backend server that reads, writes, and deploys contracts",
|
||||
link: "https://portal.thirdweb.com/engine",
|
||||
link: "https://thirdweb.com/engine",
|
||||
},
|
||||
{
|
||||
name: "Account Abstraction",
|
||||
@@ -55,7 +56,14 @@ export const products = [
|
||||
id: "insight",
|
||||
icon: InsightIcon,
|
||||
description: "Query, transform and analyze blockchain data",
|
||||
link: "https://portal.thirdweb.com/insight",
|
||||
link: "https://thirdweb.com/insight",
|
||||
},
|
||||
{
|
||||
name: "Nebula",
|
||||
id: "nebula",
|
||||
icon: NebulaIcon,
|
||||
description: "The most powerful AI for interacting with the blockchain",
|
||||
link: "https://thirdweb.com/nebula",
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ export type ChainSupportedService =
|
||||
| "connect-sdk"
|
||||
| "engine"
|
||||
| "account-abstraction"
|
||||
| "nebula"
|
||||
| "pay"
|
||||
| "rpc-edge"
|
||||
| "chainsaw"
|
||||
|
||||
+12
-12
@@ -33,9 +33,9 @@ const PackageIcon: React.FC = () => (
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,.5)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
@@ -53,9 +53,9 @@ const FileTextIcon: React.FC = () => (
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,.5)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
@@ -74,9 +74,9 @@ const CalendarIcon: React.FC = () => (
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,.5)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
@@ -94,9 +94,9 @@ const VersionIcon: React.FC = () => (
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,.5)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="6" y1="3" x2="6" y2="15" />
|
||||
<circle cx="18" cy="6" r="3" />
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
|
||||
const links: {
|
||||
name: string;
|
||||
segment: string;
|
||||
}[] = [
|
||||
{ name: "Transaction Simulator", segment: "transaction-simulator" },
|
||||
{ name: "Wei Converter", segment: "wei-converter" },
|
||||
{ name: "Hex Converter", segment: "hex-converter" },
|
||||
{ name: "Unix Time Converter", segment: "unixtime-converter" },
|
||||
{ name: "Keccak-256 Converter", segment: "keccak256-converter" },
|
||||
];
|
||||
|
||||
export function ToolsTabs() {
|
||||
const layoutSegment = useSelectedLayoutSegment() || "";
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={links.map((tab) => ({
|
||||
name: tab.name,
|
||||
href: `/tools/${tab.segment}`,
|
||||
isActive: layoutSegment === tab.segment,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { Metadata } from "next";
|
||||
import { ToolsTabs } from "./components/tabs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "thirdweb Blockchain Tools",
|
||||
@@ -11,13 +11,31 @@ export default function ToolLayout({
|
||||
children,
|
||||
}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="flex h-full flex-col gap-8">
|
||||
<main className="container flex-1 pb-20">
|
||||
<div className="h-14" />
|
||||
<ToolsTabs />
|
||||
<div className="h-8" />
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
</section>
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
label: "Transaction Simulator",
|
||||
href: "/tools/transaction-simulator",
|
||||
},
|
||||
{
|
||||
label: "Wei Converter",
|
||||
href: "/tools/wei-converter",
|
||||
},
|
||||
{
|
||||
label: "Hex Converter",
|
||||
href: "/tools/hex-converter",
|
||||
},
|
||||
{
|
||||
label: "Unix Time Converter",
|
||||
href: "/tools/unixtime-converter",
|
||||
},
|
||||
{
|
||||
label: "Keccak-256 Converter",
|
||||
href: "/tools/keccak256-converter",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+138
-106
@@ -1,118 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { ShareButton } from "../../components/share";
|
||||
import {
|
||||
formatDistanceToNow,
|
||||
formatISO9075,
|
||||
formatRFC3339,
|
||||
formatRFC7231,
|
||||
fromUnixTime,
|
||||
} from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export const UnixTimeConverter = () => {
|
||||
const getCurrentUnixMilliseconds = () => Math.floor(new Date().getTime());
|
||||
const [unix, setUnix] = useState<number>(getCurrentUnixMilliseconds());
|
||||
export function UnixTimeConverter() {
|
||||
const [unixTime, setUnixTime] = useState(() => {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
});
|
||||
|
||||
// `unix` may be in milliseconds or seconds.
|
||||
// Treat any value < 100000000000 as seconds, otherwise milliseconds.
|
||||
const isSeconds = unix < 100000000000;
|
||||
const date = new Date(isSeconds ? unix * 1000 : unix);
|
||||
const dateLocal = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
});
|
||||
const dateUTC = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
const { date, error } = useMemo(() => {
|
||||
if (Number.isNaN(unixTime) || !Number.isInteger(unixTime)) {
|
||||
return {
|
||||
error: "Invalid Unix Timestamp",
|
||||
date: null,
|
||||
};
|
||||
}
|
||||
|
||||
const date = fromUnixTime(unixTime);
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return {
|
||||
date: null,
|
||||
error: "Invalid Unix Timestamp",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
date,
|
||||
error: null,
|
||||
};
|
||||
}, [unixTime]);
|
||||
|
||||
return (
|
||||
<div className="space-y-24">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label htmlFor="unix-input" className="min-w-64 text-xl">
|
||||
Unix Time
|
||||
</Label>
|
||||
<Input
|
||||
id="unix-input"
|
||||
type="number"
|
||||
value={unix}
|
||||
onChange={(e) => {
|
||||
if (!Number.isNaN(e.target.valueAsNumber)) {
|
||||
setUnix(e.target.valueAsNumber);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a Unix time in milliseconds or seconds"
|
||||
className="p-6 text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (local)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateLocal}
|
||||
textToShow={dateLocal}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm italic">
|
||||
← {formatDistanceToNow(date, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (UTC)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateUTC}
|
||||
textToShow={dateUTC}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<ShareButton
|
||||
cta="Share on X"
|
||||
href="https://twitter.com/intent/tweet?text=Easy-to-use%20Unix%20time%20converter%20by%20thirdweb%20%F0%9F%98%8D&url=https%3A%2F%2Fthirdweb.com%2Ftools%2Funixtime-converter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader className="mb-6 border-b">
|
||||
<CardTitle className="font-bold text-2xl">
|
||||
Unix Time Converter
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="font-bold text-2xl">
|
||||
Convert Unix time in milliseconds or seconds
|
||||
</h1>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="unixTime">Enter Unix Timestamp</Label>
|
||||
<Input
|
||||
id="unixTime"
|
||||
type="number"
|
||||
value={unixTime}
|
||||
onChange={(e) => {
|
||||
setUnixTime(Number(e.target.value));
|
||||
}}
|
||||
placeholder="e.g., 1609459200"
|
||||
className="mt-1"
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-2 text-destructive-text text-sm">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Timestamps are often represented in Unix time for several key reasons:
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Simplicity</h2>
|
||||
<p>
|
||||
Counts seconds since January 1, 1970, making it easy to understand and
|
||||
manipulate.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Consistency</h2>
|
||||
<p>
|
||||
Provides a uniform time representation across systems, unaffected by
|
||||
time zones or calendar changes.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Standardization</h2>
|
||||
<p>
|
||||
Widely supported in computing, ensuring compatibility across
|
||||
platforms.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Calculation</h2>
|
||||
<p>
|
||||
Allows straightforward arithmetic operations for time differences.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Efficiency</h2>
|
||||
<p>
|
||||
Stores time as a single integer, conserving memory and computational
|
||||
resources.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Interoperability</h2>
|
||||
<p>Facilitates seamless data exchange between systems and languages.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label>Relative Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date
|
||||
? formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Local Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatLocal(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>UTC Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatUTC(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 8601</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO8601(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 9075</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO9075(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 7231</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC7231(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 3339</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC3339(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>SQL Timestamp</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatSQL(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function formatLocal(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatUTC(date: Date): string {
|
||||
return date.toUTCString();
|
||||
}
|
||||
|
||||
function isValidDate(date: Date): boolean {
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
function formatISO8601(date: Date): string {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function formatSQL(date: Date): string {
|
||||
return date.toISOString().slice(0, 19).replace("T", " ");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ClientOnly } from "../../../../components/ClientOnly/ClientOnly";
|
||||
import { UnixTimeConverter } from "./components/UnixTimeConverter";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,5 +8,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return <UnixTimeConverter />;
|
||||
return (
|
||||
<ClientOnly ssr={null}>
|
||||
<UnixTimeConverter />
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import { Box, Container, Flex } from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { LandingDynamicSelector } from "components/landing-pages/dynamic-selector";
|
||||
import { LandingEndCTA } from "components/landing-pages/end-cta";
|
||||
import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase";
|
||||
import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image";
|
||||
import { LandingShowcaseImage } from "components/landing-pages/showcase-image";
|
||||
import type { Metadata } from "next";
|
||||
import { Heading, Text } from "tw-components";
|
||||
// images
|
||||
import smartWalletMiniImage from "../../../../public/assets/product-icons/smart-wallet.png";
|
||||
import accountAbstractionImage from "../../../../public/assets/product-pages/smart-wallet/account-abstraction.png";
|
||||
import batchTxImage from "../../../../public/assets/product-pages/smart-wallet/batch-txns.png";
|
||||
import dashboardImage from "../../../../public/assets/product-pages/smart-wallet/dashboard.png";
|
||||
import desktopHero from "../../../../public/assets/product-pages/smart-wallet/desktop-hero.png";
|
||||
import fullyProgrammaticImage from "../../../../public/assets/product-pages/smart-wallet/full-programmability.png";
|
||||
import getStartedImage from "../../../../public/assets/product-pages/smart-wallet/get-started.png";
|
||||
import invisibleWalletsImage from "../../../../public/assets/product-pages/smart-wallet/invisible-wallet.png";
|
||||
import managedInfrastructureImage from "../../../../public/assets/product-pages/smart-wallet/managed-infrastructure.png";
|
||||
import mobileHero from "../../../../public/assets/product-pages/smart-wallet/mobile-hero.png";
|
||||
import pairAnyWalletImage from "../../../../public/assets/product-pages/smart-wallet/pair-any-wallet.png";
|
||||
import smartContractsImage from "../../../../public/assets/product-pages/smart-wallet/smart-contracts.png";
|
||||
import uiComponentsImage from "../../../../public/assets/product-pages/smart-wallet/ui-components.png";
|
||||
import chooseContractImage from "../../../../public/assets/product-pages/smart-wallet/which-contract.png";
|
||||
import { getAbsoluteUrl } from "../../../lib/vercel-utils";
|
||||
|
||||
const GUIDES = [
|
||||
{
|
||||
title: "The Quick-Start Guide to Account Abstraction",
|
||||
image: getStartedImage,
|
||||
link: "https://portal.thirdweb.com/wallets/smart-wallet/get-started",
|
||||
},
|
||||
{
|
||||
title: "Choosing Between Simple, Managed, & Dynamic Smart Accounts",
|
||||
image: chooseContractImage,
|
||||
link: "https://blog.thirdweb.com/smart-contract-deep-dive-building-smart-wallets-for-individuals-and-teams/",
|
||||
},
|
||||
{
|
||||
title: "How to Enable Batch Transactions with Account Abstraction",
|
||||
image: batchTxImage,
|
||||
link: "https://blog.thirdweb.com/guides/how-to-batch-transactions-with-the-thirdweb-sdk/",
|
||||
},
|
||||
];
|
||||
|
||||
const TRACKING_CATEGORY = "smart-wallet-landing";
|
||||
|
||||
const title = "The Complete Account Abstraction Toolkit";
|
||||
const description =
|
||||
"Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Learn more.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/smart-wallet.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Container
|
||||
maxW="container.page"
|
||||
as={Flex}
|
||||
flexDir="column"
|
||||
gap={{ base: "80px", md: "120px" }}
|
||||
>
|
||||
<LandingHeroWithSideImage
|
||||
miniTitle="Powered by ERC-4337"
|
||||
title="The complete toolkit for"
|
||||
titleWithGradient="Account Abstraction"
|
||||
subtitle="Transform your app's user experience with signless transactions, multi-signature security, account recovery and more."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaLink="https://portal.thirdweb.com/connect/account-abstraction/overview"
|
||||
contactUsTitle="Book Demo"
|
||||
gradient="linear(to-r, #4490FF, #4490FF)"
|
||||
image={desktopHero}
|
||||
mobileImage={mobileHero}
|
||||
miniImage={smartWalletMiniImage}
|
||||
/>
|
||||
<LandingDynamicSelector
|
||||
title="What can you build with account abstraction?"
|
||||
blackToWhiteTitle=""
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
items={[
|
||||
{
|
||||
title: "Transform your apps UX",
|
||||
description:
|
||||
"Infinitely-customizable wallet transactions — with spend limits, batch transactions, and a signless & gasless combo for web2 users.",
|
||||
Component: (
|
||||
<ChakraNextImage
|
||||
src={invisibleWalletsImage}
|
||||
alt=""
|
||||
borderRadius="lg"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Instant onboarding for every user",
|
||||
description:
|
||||
"Auth for the most popular web3 wallets and web2 login flows — with just an email, phone number, social account, or passkeys.",
|
||||
Component: (
|
||||
<ChakraNextImage
|
||||
src={pairAnyWalletImage}
|
||||
alt=""
|
||||
borderRadius="lg"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Enterprise-grade security",
|
||||
description:
|
||||
"Wallet recovery, 2FA, and multi-signature support for ultimate peace of mind — for users & teams.",
|
||||
Component: (
|
||||
<ChakraNextImage
|
||||
src={fullyProgrammaticImage}
|
||||
alt=""
|
||||
borderRadius="lg"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Flex flexDir="column" gap={6} alignItems="center">
|
||||
<Heading
|
||||
as="h2"
|
||||
size="display.md"
|
||||
textAlign="center"
|
||||
px={{ base: 2, md: 0 }}
|
||||
>
|
||||
An all-in-one solution for
|
||||
<Box bgGradient="linear(to-r, #3385FF, #7BB0FF)" bgClip="text">
|
||||
Account Abstraction
|
||||
</Box>
|
||||
</Heading>
|
||||
<Text textAlign="center" size="body.xl" w={{ base: "100%", md: "60%" }}>
|
||||
Implement account abstraction into any web3 app — with a best-in-class
|
||||
SDK, full wallet customizability, and managed infrastructure.
|
||||
</Text>
|
||||
</Flex>
|
||||
<LandingShowcaseImage
|
||||
miniTitle="Smart Contracts"
|
||||
titleWithGradient="Fast Account Abstraction"
|
||||
title="factory deployment."
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
description="Pre-built, audited, & customizable factory contracts with options for simple, managed, or dynamic permissions — deployable to any EVM network."
|
||||
image={smartContractsImage}
|
||||
/>
|
||||
<LandingShowcaseImage
|
||||
miniTitle="SDK"
|
||||
titleWithGradient="Integrate account abstraction"
|
||||
title="into your web3 apps."
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
description="A best-in-class SDK with everything you need to build apps powered by account abstraction & generate accounts for your users — with full ERC-4337 compatibility."
|
||||
image={accountAbstractionImage}
|
||||
imagePosition="left"
|
||||
/>
|
||||
<LandingShowcaseImage
|
||||
miniTitle="UI Components"
|
||||
titleWithGradient="Drag and drop"
|
||||
title="UI Components."
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
description="The easiest way to give accounts to your users with account abstraction — with UI components that are purpose-built for account abstraction & compatible with any existing ERC-4337 account, out of the box."
|
||||
image={uiComponentsImage}
|
||||
/>
|
||||
<LandingShowcaseImage
|
||||
miniTitle="Managed Infrastructure"
|
||||
titleWithGradient="Support millions"
|
||||
title="of users."
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
description="Managed infrastructure with everything you need to scale to millions of users — handling bundlers, paymasters, & all of the complexity so you can focus on building your app."
|
||||
image={managedInfrastructureImage}
|
||||
imagePosition="left"
|
||||
/>
|
||||
<LandingShowcaseImage
|
||||
miniTitle="Dashboard & Analytics"
|
||||
titleWithGradient="Manage accounts"
|
||||
title="& view onchain analytics."
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
description="A single source of truth to view accounts, manage signers, and view balances for your web3 app — complete with onchain analytics."
|
||||
image={dashboardImage}
|
||||
/>
|
||||
<LandingGuidesShowcase
|
||||
title="Get started with Account Abstraction"
|
||||
category={TRACKING_CATEGORY}
|
||||
description="See our quick-start guides to implement account abstraction into your web3 app"
|
||||
solution="Account Abstraction"
|
||||
guides={GUIDES}
|
||||
/>
|
||||
|
||||
<LandingEndCTA
|
||||
title="Add account abstraction"
|
||||
titleWithGradient="to your web3 app."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaLink="https://portal.thirdweb.com/connect/account-abstraction/overview"
|
||||
gradient="linear(to-r, #3385FF, #7BB0FF)"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -70,6 +70,11 @@ export function AccountHeader(props: {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
}}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
enableNebulaServiceByDefault={
|
||||
createProjectDialogState.isOpen &&
|
||||
createProjectDialogState.team.enabledScopes.includes("nebula")
|
||||
|
||||
@@ -17,7 +17,7 @@ export type AccountHeaderCompProps = {
|
||||
connectButton: React.ReactNode;
|
||||
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
|
||||
createProject: (team: Team) => void;
|
||||
account: Pick<Account, "email" | "id"> | undefined;
|
||||
account: Pick<Account, "email" | "id" | "image"> | undefined;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
@@ -41,10 +41,9 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
|
||||
href="/account"
|
||||
className="flex flex-row items-center gap-2 font-normal text-sm"
|
||||
>
|
||||
{/* TODO - set account Image */}
|
||||
<GradientAvatar
|
||||
id={props.account?.id || "default"}
|
||||
src={""}
|
||||
src={props.account?.image || ""}
|
||||
className="size-6"
|
||||
client={props.client}
|
||||
/>
|
||||
@@ -90,10 +89,9 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
|
||||
"flex flex-row items-center gap-2 font-normal text-foreground text-sm",
|
||||
)}
|
||||
>
|
||||
{/* TODO - set account image */}
|
||||
<GradientAvatar
|
||||
id={props.account?.id}
|
||||
src={props.account ? "" : undefined}
|
||||
src={props.account?.image || ""}
|
||||
className="size-6"
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { updateAccount } from "@/actions/updateAccount";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
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: {
|
||||
@@ -23,9 +24,25 @@ export function AccountSettingsPage(props: {
|
||||
</div>
|
||||
<div className="container max-w-[950px] grow pt-8 pb-20">
|
||||
<AccountSettingsPageUI
|
||||
// TODO - remove hide props these when these fields are functional
|
||||
hideAvatar
|
||||
hideDeleteAccount
|
||||
client={props.client}
|
||||
updateAccountAvatar={async (file) => {
|
||||
let uri: string | undefined = undefined;
|
||||
|
||||
if (file) {
|
||||
// upload to IPFS
|
||||
uri = await upload({
|
||||
client: props.client,
|
||||
files: [file],
|
||||
});
|
||||
}
|
||||
|
||||
await updateAccount({
|
||||
image: uri,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
}}
|
||||
account={props.account}
|
||||
updateEmailWithOTP={async (otp) => {
|
||||
const res = await confirmEmailWithOTP(otp);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
@@ -33,6 +34,8 @@ export const Mobile: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const client = getThirdwebClient();
|
||||
|
||||
function Variants() {
|
||||
const [isVerifiedEmail, setIsVerifiedEmail] = useState(true);
|
||||
const [sendEmailFails, setSendEmailFails] = useState(false);
|
||||
@@ -74,6 +77,10 @@ function Variants() {
|
||||
? new Date().toISOString()
|
||||
: undefined,
|
||||
}}
|
||||
client={client}
|
||||
updateAccountAvatar={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
updateEmailWithOTP={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
if (emailConfirmationFails) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -39,30 +40,35 @@ import { EllipsisIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { z } from "zod";
|
||||
import { FileInput } from "../../../components/shared/FileInput";
|
||||
|
||||
type MinimalAccount = Pick<
|
||||
Account,
|
||||
"name" | "email" | "emailConfirmedAt" | "unconfirmedEmail"
|
||||
"name" | "email" | "emailConfirmedAt" | "unconfirmedEmail" | "image"
|
||||
>;
|
||||
|
||||
export function AccountSettingsPageUI(props: {
|
||||
account: MinimalAccount;
|
||||
// TODO - remove hide props these when these fields are functional
|
||||
hideAvatar?: boolean;
|
||||
hideDeleteAccount?: boolean;
|
||||
sendEmail: (email: string) => Promise<void>;
|
||||
updateName: (name: string) => Promise<void>;
|
||||
updateEmailWithOTP: (otp: string) => Promise<void>;
|
||||
updateAccountAvatar: (avatar: File | undefined) => Promise<void>;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{!props.hideAvatar && <AccountAvatarFormControl />}
|
||||
<AccountNameFormControl
|
||||
name={props.account.name || ""}
|
||||
updateName={(name) => props.updateName(name)}
|
||||
/>
|
||||
<AccountAvatarFormControl
|
||||
updateAccountAvatar={props.updateAccountAvatar}
|
||||
avatar={props.account.image || undefined}
|
||||
client={props.client}
|
||||
/>
|
||||
<AccountEmailFormControl
|
||||
email={props.account.email || ""}
|
||||
status={props.account.emailConfirmedAt ? "verified" : "unverified"}
|
||||
@@ -75,18 +81,24 @@ export function AccountSettingsPageUI(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AccountAvatarFormControl() {
|
||||
function AccountAvatarFormControl(props: {
|
||||
updateAccountAvatar: (avatar: File | undefined) => Promise<void>;
|
||||
avatar: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const accountAvatarUrl = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: props.avatar,
|
||||
});
|
||||
const [avatar, setAvatar] = useState<File>();
|
||||
|
||||
// TODO - implement
|
||||
const updateAvatarMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
mutationFn: (_avatar: File | undefined) => {
|
||||
return props.updateAccountAvatar(_avatar);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSave() {
|
||||
const promises = updateAvatarMutation.mutateAsync();
|
||||
const promises = updateAvatarMutation.mutateAsync(avatar);
|
||||
toast.promise(promises, {
|
||||
success: "Account avatar updated successfully",
|
||||
error: "Failed to update account avatar",
|
||||
@@ -118,6 +130,7 @@ function AccountAvatarFormControl() {
|
||||
setValue={setAvatar}
|
||||
className="w-20 rounded-full lg:w-28"
|
||||
disableHelperText
|
||||
fileUrl={accountAvatarUrl}
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -30,19 +30,27 @@ export function SecondaryNavLinks() {
|
||||
<ResourcesDropdownButton />
|
||||
|
||||
<Link
|
||||
href="https://portal.thirdweb.com"
|
||||
className="text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
href="https://thirdweb.com/support"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
target="_blank"
|
||||
href="/support"
|
||||
className="text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/"
|
||||
className="text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
href="https://feedback.thirdweb.com"
|
||||
className="text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
Feedback
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { ThirdwebClient } from "thirdweb";
|
||||
export function AccountButton(props: {
|
||||
logout: () => void;
|
||||
connectButton: React.ReactNode;
|
||||
account?: Pick<Account, "email" | "id">;
|
||||
account?: Pick<Account, "email" | "id" | "image">;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
@@ -35,11 +35,10 @@ export function AccountButton(props: {
|
||||
>
|
||||
{/* Don't remove the div */}
|
||||
<div>
|
||||
{/* TODO - set account image */}
|
||||
<GradientAvatar
|
||||
id={props.account?.id || "default"}
|
||||
src={""}
|
||||
className="size-9"
|
||||
src={props.account?.image || ""}
|
||||
className="size-9 border"
|
||||
client={props.client}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -159,6 +159,34 @@ export function MobileBurgerMenuButton(
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<div className="flex flex-col gap-5">
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com"
|
||||
className="text-muted-foreground hover:text-foreground "
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
target="_blank"
|
||||
href="/support"
|
||||
className="text-muted-foreground hover:text-foreground "
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://feedback.thirdweb.com"
|
||||
className="text-muted-foreground hover:text-foreground "
|
||||
>
|
||||
Feedback
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Separator />
|
||||
<div className="h-6" />
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
accentText: "hsl(var(--link-foreground))",
|
||||
accentButtonBg: "hsl(var(--primary))",
|
||||
accentButtonText: "hsl(var(--primary-foreground))",
|
||||
primaryButtonBg: "hsl(var(--background))",
|
||||
primaryButtonText: "hsl(var(--foreground))",
|
||||
primaryButtonBg: "hsl(var(--inverted))",
|
||||
primaryButtonText: "hsl(var(--inverted-foreground))",
|
||||
secondaryButtonText: "hsl(var(--secondary-foreground))",
|
||||
tooltipBg: "hsl(var(--popover))",
|
||||
tooltipText: "hsl(var(--popover-foreground))",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
@@ -82,7 +81,6 @@ export default function RootLayout({
|
||||
<EnsureValidConnectedWalletLoginServer />
|
||||
</Suspense>
|
||||
</AppRouterProviders>
|
||||
<Toaster richColors />
|
||||
<DashboardRouterTopProgressBar />
|
||||
<NextTopLoader
|
||||
color="hsl(var(--primary))"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
import { Suspense, lazy, useEffect, useState } from "react";
|
||||
import { ConnectEmbed, useActiveWalletConnectionStatus } from "thirdweb/react";
|
||||
import { createWallet, inAppWallet } from "thirdweb/wallets";
|
||||
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
|
||||
@@ -55,7 +55,15 @@ export function LoginAndOnboardingPage(props: {
|
||||
<ThirdwebMiniLogo className="size-7 md:size-8" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/"
|
||||
className="px-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/support"
|
||||
target="_blank"
|
||||
@@ -65,11 +73,11 @@ export function LoginAndOnboardingPage(props: {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/"
|
||||
className="px-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
href="https://feedback.thirdweb.com"
|
||||
className="px-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
Feedback
|
||||
</Link>
|
||||
|
||||
<ColorModeToggle />
|
||||
@@ -154,27 +162,6 @@ function PageContent(props: {
|
||||
router.replace(props.redirectPath);
|
||||
}
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function onLogin() {
|
||||
const account = await getRawAccountAction();
|
||||
|
||||
@@ -193,6 +180,38 @@ function PageContent(props: {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
// if suddenly disconnected
|
||||
if (connectionStatus !== "connected" && screen.id !== "login") {
|
||||
setScreen({ id: "login" });
|
||||
}
|
||||
}, [connectionStatus, screen.id]);
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
onLogout={() => {
|
||||
setScreen({ id: "login" });
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type AccountValidationSchema,
|
||||
accountValidationSchema,
|
||||
@@ -81,15 +82,21 @@ export const AccountForm: React.FC<AccountFormProps> = ({
|
||||
data,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
|
||||
if (
|
||||
onDuplicateError &&
|
||||
error?.message.match(/email address already exists/)
|
||||
) {
|
||||
onDuplicateError(values.email);
|
||||
return;
|
||||
} else if (error.message.includes("INVALID_EMAIL_ADDRESS")) {
|
||||
toast.error("Invalid Email Address");
|
||||
} else {
|
||||
toast.error(error.message || "Failed to update account");
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
category: "account",
|
||||
action: "update",
|
||||
|
||||
@@ -27,9 +27,10 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="starter"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get started for free",
|
||||
title: "Get Started for free",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "starterPlan",
|
||||
},
|
||||
}}
|
||||
redirectPath={props.redirectPath}
|
||||
@@ -40,15 +41,14 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="growth"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Claim your 1-month free",
|
||||
hint: "Your free trial will end after 30 days.",
|
||||
title: "Get Started with Growth",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "growthPlan",
|
||||
},
|
||||
variant: "default",
|
||||
}}
|
||||
canTrialGrowth={props.canTrialGrowth}
|
||||
canTrialGrowth={false}
|
||||
highlighted
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={props.redirectToCheckout}
|
||||
|
||||
@@ -14,12 +14,14 @@ type OnboardingGeneralProps = {
|
||||
account: Account;
|
||||
onSave: (email: string) => void;
|
||||
onDuplicate: (email: string) => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
account,
|
||||
onSave,
|
||||
onDuplicate,
|
||||
onLogout,
|
||||
}) => {
|
||||
const [existing, setExisting] = useState(false);
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -27,6 +29,7 @@ export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
|
||||
async function handleLogout() {
|
||||
await doLogout();
|
||||
onLogout();
|
||||
if (activeWallet) {
|
||||
disconnect(activeWallet);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type OnboardingScreen =
|
||||
function OnboardingUI(props: {
|
||||
account: Account;
|
||||
onComplete: () => void;
|
||||
onLogout: () => void;
|
||||
// path to redirect from stripe
|
||||
redirectPath: string;
|
||||
redirectToCheckout: RedirectBillingCheckoutAction;
|
||||
@@ -68,6 +69,7 @@ function OnboardingUI(props: {
|
||||
{screen.id === "onboarding" && (
|
||||
<OnboardingGeneral
|
||||
account={account}
|
||||
onLogout={props.onLogout}
|
||||
onSave={(email) => {
|
||||
setUpdatedEmail(email);
|
||||
setScreen({
|
||||
|
||||
@@ -2,42 +2,33 @@ import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
|
||||
// TODO - copy the source of this library to dashboard
|
||||
import { stream } from "fetch-event-stream";
|
||||
import type { NebulaTxData } from "../components/Chats";
|
||||
import type { ExecuteConfig } from "./types";
|
||||
|
||||
export type ContextFilters = {
|
||||
chainIds?: string[];
|
||||
contractAddresses?: string[];
|
||||
walletAddresses?: string[];
|
||||
export type NebulaContext = {
|
||||
chainIds: string[] | null;
|
||||
walletAddress: string | null;
|
||||
};
|
||||
|
||||
export async function promptNebula(params: {
|
||||
message: string;
|
||||
sessionId: string;
|
||||
config: ExecuteConfig | null;
|
||||
authToken: string;
|
||||
handleStream: (res: ChatStreamedResponse) => void;
|
||||
abortController: AbortController;
|
||||
contextFilters: undefined | ContextFilters;
|
||||
context: undefined | NebulaContext;
|
||||
}) {
|
||||
const body: Record<string, string | boolean | object> = {
|
||||
message: params.message,
|
||||
user_id: "default-user",
|
||||
session_id: params.sessionId,
|
||||
stream: true,
|
||||
session_id: params.sessionId,
|
||||
};
|
||||
|
||||
if (params.contextFilters) {
|
||||
body.context_filter = {
|
||||
chain_ids: params.contextFilters.chainIds || [],
|
||||
contract_addresses: params.contextFilters.contractAddresses || [],
|
||||
wallet_addresses: params.contextFilters.walletAddresses || [],
|
||||
if (params.context) {
|
||||
body.context = {
|
||||
chain_ids: params.context.chainIds || [],
|
||||
wallet_address: params.context.walletAddress,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.config) {
|
||||
body.execute_config = params.config;
|
||||
}
|
||||
|
||||
const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
|
||||
import { fetchWithAuthToken } from "../../../../utils/fetchWithAuthToken";
|
||||
import type { ContextFilters } from "./chat";
|
||||
import type { NebulaContext } from "./chat";
|
||||
import type {
|
||||
DeletedSessionInfo,
|
||||
ExecuteConfig,
|
||||
SessionInfo,
|
||||
TruncatedSessionInfo,
|
||||
UpdatedSessionInfo,
|
||||
} from "./types";
|
||||
|
||||
// TODO - get the spec for return types on /session POST and PUT
|
||||
|
||||
export async function createSession(params: {
|
||||
authToken: string;
|
||||
config: ExecuteConfig | null;
|
||||
contextFilters: ContextFilters | undefined;
|
||||
context: NebulaContext | undefined;
|
||||
}) {
|
||||
const body: Record<string, string | boolean | object> = {
|
||||
can_execute: !!params.config,
|
||||
};
|
||||
if (params.config) {
|
||||
body.execute_config = params.config;
|
||||
}
|
||||
const body: Record<string, string | boolean | object> = {};
|
||||
|
||||
if (params.contextFilters) {
|
||||
body.context_filter = {
|
||||
chain_ids: params.contextFilters.chainIds || [],
|
||||
contract_addresses: params.contextFilters.contractAddresses || [],
|
||||
wallet_addresses: params.contextFilters.walletAddresses || [],
|
||||
if (params.context) {
|
||||
body.context = {
|
||||
chain_ids: params.context.chainIds || [],
|
||||
wallet_address: params.context.walletAddress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,22 +38,15 @@ export async function createSession(params: {
|
||||
|
||||
export async function updateSession(params: {
|
||||
authToken: string;
|
||||
config: ExecuteConfig | null;
|
||||
sessionId: string;
|
||||
contextFilters: ContextFilters | undefined;
|
||||
contextFilters: NebulaContext | undefined;
|
||||
}) {
|
||||
const body: Record<string, string | boolean | object> = {
|
||||
can_execute: !!params.config,
|
||||
};
|
||||
if (params.config) {
|
||||
body.execute_config = params.config;
|
||||
}
|
||||
const body: Record<string, string | boolean | object> = {};
|
||||
|
||||
if (params.contextFilters) {
|
||||
body.context_filter = {
|
||||
body.context = {
|
||||
chain_ids: params.contextFilters.chainIds || [],
|
||||
contract_addresses: params.contextFilters.contractAddresses || [],
|
||||
wallet_addresses: params.contextFilters.walletAddresses || [],
|
||||
wallet_address: params.contextFilters.walletAddress,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
type EngineConfig = {
|
||||
mode: "engine";
|
||||
engine_url: string;
|
||||
engine_authorization_token: string;
|
||||
engine_backend_wallet_address: string;
|
||||
};
|
||||
|
||||
type SessionKeyConfig = {
|
||||
mode: "session_key";
|
||||
smart_account_address: string;
|
||||
smart_account_factory_address: string;
|
||||
smart_account_session_key: string;
|
||||
};
|
||||
|
||||
type ClientConfig = {
|
||||
mode: "client";
|
||||
signer_wallet_address: string;
|
||||
};
|
||||
|
||||
export type ExecuteConfig = EngineConfig | SessionKeyConfig | ClientConfig;
|
||||
|
||||
type SessionContextFilter = {
|
||||
chain_ids: string[] | null;
|
||||
contract_addresses: string[] | null;
|
||||
wallet_addresses: string[] | null;
|
||||
wallet_address: string | null;
|
||||
};
|
||||
|
||||
export type SessionInfo = {
|
||||
@@ -31,7 +9,6 @@ export type SessionInfo = {
|
||||
modal_name: string;
|
||||
archive_at: string | null;
|
||||
can_execute: boolean;
|
||||
execute_config: ExecuteConfig | null;
|
||||
created_at: string;
|
||||
deleted_at: string | null;
|
||||
history: Array<{
|
||||
@@ -43,7 +20,7 @@ export type SessionInfo = {
|
||||
archived_at: string | null;
|
||||
title: string | null;
|
||||
is_public: boolean | null;
|
||||
context_filter: SessionContextFilter | null;
|
||||
context: SessionContextFilter | null;
|
||||
// memory
|
||||
// action: array<object> | null; <-- type of this is not available on https://nebula-api.thirdweb-dev.com/docs#/default/get_session_session__session_id__get
|
||||
};
|
||||
@@ -52,8 +29,7 @@ export type UpdatedSessionInfo = {
|
||||
title: string;
|
||||
modal_name: string;
|
||||
account_id: string;
|
||||
execute_config: ExecuteConfig | null;
|
||||
context_filter: SessionContextFilter | null;
|
||||
context: SessionContextFilter | null;
|
||||
};
|
||||
|
||||
export type DeletedSessionInfo = {
|
||||
|
||||
@@ -181,6 +181,7 @@ function SessionCard(props: {
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={`/chat/${props.session.id}`}
|
||||
prefetch={false}
|
||||
>
|
||||
{props.session.title || "Untitled"}
|
||||
</Link>
|
||||
|
||||
@@ -12,15 +12,15 @@ import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { type ContextFilters, promptNebula } from "../api/chat";
|
||||
import { type NebulaContext, promptNebula } from "../api/chat";
|
||||
import { createSession, updateSession } from "../api/session";
|
||||
import type { ExecuteConfig, SessionInfo } from "../api/types";
|
||||
import type { SessionInfo } from "../api/types";
|
||||
import { newChatPageUrlStore, newSessionsStore } from "../stores";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
import { type ChatMessage, Chats } from "./Chats";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
import ContextFiltersButton, { ContextFiltersForm } from "./ContextFilters";
|
||||
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
|
||||
|
||||
export function ChatPageContent(props: {
|
||||
@@ -81,27 +81,26 @@ export function ChatPageContent(props: {
|
||||
useState(false);
|
||||
|
||||
const [contextFilters, _setContextFilters] = useState<
|
||||
ContextFilters | undefined
|
||||
NebulaContext | undefined
|
||||
>(() => {
|
||||
const contextFilterRes = props.session?.context_filter;
|
||||
const value: ContextFilters = {
|
||||
chainIds: contextFilterRes?.chain_ids || undefined,
|
||||
contractAddresses: contextFilterRes?.contract_addresses || undefined,
|
||||
walletAddresses: contextFilterRes?.wallet_addresses || undefined,
|
||||
const contextRes = props.session?.context;
|
||||
const value: NebulaContext = {
|
||||
chainIds: contextRes?.chain_ids || null,
|
||||
walletAddress: contextRes?.wallet_address || null,
|
||||
};
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const setContextFilters = useCallback((v: ContextFilters | undefined) => {
|
||||
const setContextFilters = useCallback((v: NebulaContext | undefined) => {
|
||||
_setContextFilters(v);
|
||||
setHasUserUpdatedContextFilters(true);
|
||||
}, []);
|
||||
|
||||
const isNewSession = !props.session;
|
||||
|
||||
// if this is a new session, user has not manually updated context filters
|
||||
// update the context filters to the current user's wallet address and chain id
|
||||
// if this is a new session, user has not manually updated context
|
||||
// update the context to the current user's wallet address and chain id
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isNewSession || hasUserUpdatedContextFilters) {
|
||||
@@ -109,11 +108,16 @@ export function ChatPageContent(props: {
|
||||
}
|
||||
|
||||
_setContextFilters((_contextFilters) => {
|
||||
const updatedContextFilters: ContextFilters = _contextFilters
|
||||
? { ..._contextFilters }
|
||||
: {};
|
||||
const updatedContextFilters: NebulaContext = _contextFilters
|
||||
? {
|
||||
..._contextFilters,
|
||||
}
|
||||
: {
|
||||
chainIds: [],
|
||||
walletAddress: null,
|
||||
};
|
||||
|
||||
updatedContextFilters.walletAddresses = address ? [address] : [];
|
||||
updatedContextFilters.walletAddress = address || null;
|
||||
updatedContextFilters.chainIds = activeChain
|
||||
? [activeChain.id.toString()]
|
||||
: [];
|
||||
@@ -122,15 +126,6 @@ export function ChatPageContent(props: {
|
||||
});
|
||||
}, [address, isNewSession, hasUserUpdatedContextFilters, activeChain]);
|
||||
|
||||
const config: ExecuteConfig | null = useMemo(() => {
|
||||
return address
|
||||
? {
|
||||
mode: "client",
|
||||
signer_wallet_address: address,
|
||||
}
|
||||
: null;
|
||||
}, [address]);
|
||||
|
||||
const [sessionId, _setSessionId] = useState<string | undefined>(
|
||||
props.session?.id,
|
||||
);
|
||||
@@ -163,12 +158,11 @@ export function ChatPageContent(props: {
|
||||
const initSession = useCallback(async () => {
|
||||
const session = await createSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
contextFilters,
|
||||
context: contextFilters,
|
||||
});
|
||||
setSessionId(session.id);
|
||||
return session;
|
||||
}, [config, contextFilters, props.authToken, setSessionId]);
|
||||
}, [contextFilters, props.authToken, setSessionId]);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
@@ -221,7 +215,6 @@ export function ChatPageContent(props: {
|
||||
abortController,
|
||||
message: message,
|
||||
sessionId: currentSessionId,
|
||||
config: config,
|
||||
authToken: props.authToken,
|
||||
handleStream(res) {
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -308,7 +301,7 @@ export function ChatPageContent(props: {
|
||||
}
|
||||
}
|
||||
},
|
||||
contextFilters: contextFilters,
|
||||
context: contextFilters,
|
||||
});
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -338,7 +331,6 @@ export function ChatPageContent(props: {
|
||||
[
|
||||
sessionId,
|
||||
contextFilters,
|
||||
config,
|
||||
props.authToken,
|
||||
messages.length,
|
||||
initSession,
|
||||
@@ -362,69 +354,91 @@ export function ChatPageContent(props: {
|
||||
|
||||
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
|
||||
|
||||
const handleUpdateContextFilters = async (
|
||||
values: NebulaContext | undefined,
|
||||
) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<WalletDisconnectedDialog
|
||||
open={showConnectModal}
|
||||
onOpenChange={setShowConnectModal}
|
||||
/>
|
||||
<header className="flex justify-start border-b bg-background p-4">
|
||||
<header className="flex justify-between border-b bg-background p-4 xl:hidden">
|
||||
<ContextFiltersButton
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
updateContextFilters={async (values) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
}}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
/>
|
||||
</header>
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex grow overflow-hidden">
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
</div>
|
||||
<aside className="hidden w-[360px] flex-col border-l bg-card pt-4 xl:flex">
|
||||
<div className="px-4">
|
||||
<h3 className="font-semibold text-lg tracking-tight">Context</h3>
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Provide context to Nebula for your prompts
|
||||
</p>
|
||||
</div>
|
||||
<ContextFiltersForm
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
modal={undefined}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
formBodyClassName="px-4"
|
||||
formActionContainerClassName="px-4 border-t-0 pt-0 bg-transparent"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
FileCode2Icon,
|
||||
MessageSquareShareIcon,
|
||||
MessagesSquareIcon,
|
||||
SquareDashedBottomCodeIcon,
|
||||
TextIcon,
|
||||
@@ -29,8 +31,11 @@ export function ChatSidebar(props: {
|
||||
return (
|
||||
<div className="flex h-full flex-col p-2">
|
||||
<div className="flex items-center justify-start gap-3 p-2 lg:justify-between">
|
||||
<Link href="/">
|
||||
<NebulaIcon className="size-8 text-foreground" />
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<NebulaIcon className="size-8 text-foreground" aria-label="Nebula" />
|
||||
<span className="font-semibold text-lg tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Badge variant="secondary" className="gap-1 py-1">
|
||||
@@ -48,6 +53,13 @@ export function ChatSidebar(props: {
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/nebula/api-reference"
|
||||
icon={FileCode2Icon}
|
||||
label="API Reference"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="/chat/history"
|
||||
icon={MessagesSquareIcon}
|
||||
@@ -79,7 +91,14 @@ export function ChatSidebar(props: {
|
||||
</ScrollShadow>
|
||||
)}
|
||||
|
||||
<div className="mb-3 border-b border-dashed pt-2 pb-3">
|
||||
<div className="mb-3 border-y border-dashed py-3">
|
||||
<SidebarIconLink
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSeM3fJRyywihRZUF1fiTNKEpJ_AzAcohRwXPpLr_3zxQ6W-tg/viewform?usp=sharing"
|
||||
icon={MessageSquareShareIcon}
|
||||
label="Take our quick survey!"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/changelog"
|
||||
icon={TextIcon}
|
||||
@@ -116,6 +135,7 @@ function SidebarIconLink(props: {
|
||||
href={props.href}
|
||||
target={props.target}
|
||||
className="!justify-start !px-3 w-full gap-2.5 rounded-lg text-left"
|
||||
prefetch={false}
|
||||
>
|
||||
<props.icon className="size-4" />
|
||||
{props.label}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import type { ContextFilters } from "../api/chat";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
|
||||
const meta = {
|
||||
@@ -37,6 +37,7 @@ function Story() {
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["137"],
|
||||
walletAddress: null,
|
||||
}}
|
||||
label="1 chain"
|
||||
/>
|
||||
@@ -44,57 +45,25 @@ function Story() {
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["137", "10", "421614"],
|
||||
walletAddress: null,
|
||||
}}
|
||||
label="Few chains"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
contractAddresses: ["0x1E51e33F9838A5a043E099C60409f62aA564272f"],
|
||||
}}
|
||||
label="1 contract"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
contractAddresses: [
|
||||
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
|
||||
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
|
||||
],
|
||||
}}
|
||||
label="Few contracts"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
walletAddresses: ["0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37"],
|
||||
walletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
chainIds: null,
|
||||
}}
|
||||
label="1 wallet"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
walletAddresses: [
|
||||
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
|
||||
],
|
||||
}}
|
||||
label="Few wallets"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["137", "10", "421614"],
|
||||
contractAddresses: [
|
||||
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
|
||||
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
|
||||
],
|
||||
walletAddresses: [
|
||||
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
|
||||
],
|
||||
walletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
}}
|
||||
label="chains + wallets + contracts"
|
||||
label="chains + wallet"
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
@@ -103,10 +72,10 @@ function Story() {
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
contextFilters: ContextFilters | undefined;
|
||||
contextFilters: NebulaContext | undefined;
|
||||
}) {
|
||||
const [contextFilters, setContextFilters] = useState<
|
||||
ContextFilters | undefined
|
||||
NebulaContext | undefined
|
||||
>(props.contextFilters);
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
@@ -114,7 +83,7 @@ function Variant(props: {
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
updateContextFilters={async (values) => {
|
||||
console.log("Updating context filters", values);
|
||||
console.log("Updating context", values);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -15,15 +16,14 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { AutoResizeTextarea } from "@/components/ui/textarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { SlidersHorizontalIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -31,75 +31,53 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { isAddress } from "thirdweb";
|
||||
import { z } from "zod";
|
||||
import type { ContextFilters } from "../api/chat";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
|
||||
export default function ContextFiltersButton(props: {
|
||||
contextFilters: ContextFilters | undefined;
|
||||
setContextFilters: (filters: ContextFilters | undefined) => void;
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
contextFilters: NebulaContext | undefined;
|
||||
setContextFilters: (filters: NebulaContext | undefined) => void;
|
||||
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const chainIds = props.contextFilters?.chainIds;
|
||||
const contractAddresses = props.contextFilters?.contractAddresses;
|
||||
const walletAddresses = props.contextFilters?.walletAddresses;
|
||||
const walletAddress = props.contextFilters?.walletAddress;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline" className="max-w-full gap-2">
|
||||
<SlidersHorizontalIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
Context Filters
|
||||
Context
|
||||
<div className="flex gap-1 overflow-hidden">
|
||||
{chainIds && chainIds.length > 0 && (
|
||||
<Badge className="gap-1 truncate" variant="secondary">
|
||||
Chain
|
||||
<span className="max-sm:hidden">{chainIds.join(", ")}</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{contractAddresses && contractAddresses.length > 0 && (
|
||||
<Badge className="gap-1 truncate" variant="secondary">
|
||||
Contract
|
||||
<span className="max-sm:hidden">
|
||||
{contractAddresses
|
||||
.map((add) => `${add.trim().slice(0, 6)}..`)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{walletAddresses && walletAddresses.length > 0 && (
|
||||
{walletAddress && (
|
||||
<Badge className="gap-1 truncate" variant="secondary">
|
||||
Wallet
|
||||
<span className="max-sm:hidden">
|
||||
{walletAddresses
|
||||
.map((add) => `${add.trim().slice(0, 6)}..`)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-0">
|
||||
<ContextFilterDialogContent
|
||||
contextFilters={props.contextFilters}
|
||||
isPending={updateMutation.isPending}
|
||||
updateFilters={async (data) => {
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
});
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">Context</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
setIsOpen(false);
|
||||
});
|
||||
<ContextFiltersForm
|
||||
contextFilters={props.contextFilters}
|
||||
updateContextFilters={props.updateContextFilters}
|
||||
setContextFilters={props.setContextFilters}
|
||||
modal={{
|
||||
close: () => setIsOpen(false),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -107,17 +85,21 @@ export default function ContextFiltersButton(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const commaSeparateListOfAddresses = z.string().refine(
|
||||
(s) => {
|
||||
if (s.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
return s.split(",").every((s) => isAddress(s.trim()));
|
||||
},
|
||||
{
|
||||
message: "Must be a comma-separated list of valid addresses",
|
||||
},
|
||||
);
|
||||
const optionalAddressSchema = z
|
||||
.string()
|
||||
.transform((v) => v.trim())
|
||||
.optional()
|
||||
.refine(
|
||||
(s) => {
|
||||
if (!s) {
|
||||
return true;
|
||||
}
|
||||
return isAddress(s);
|
||||
},
|
||||
{
|
||||
message: "Must be a valid addresses",
|
||||
},
|
||||
);
|
||||
|
||||
const formSchema = z.object({
|
||||
chainIds: z.string().refine(
|
||||
@@ -134,64 +116,71 @@ const formSchema = z.object({
|
||||
message: "Chain IDs must be a comma-separated list of integers",
|
||||
},
|
||||
),
|
||||
contractAddresses: commaSeparateListOfAddresses,
|
||||
walletAddresses: commaSeparateListOfAddresses,
|
||||
walletAddress: optionalAddressSchema,
|
||||
});
|
||||
|
||||
function ContextFilterDialogContent(props: {
|
||||
contextFilters: ContextFilters | undefined;
|
||||
updateFilters: (filters: ContextFilters | undefined) => void;
|
||||
isPending: boolean;
|
||||
export function ContextFiltersForm(props: {
|
||||
contextFilters: NebulaContext | undefined;
|
||||
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
|
||||
setContextFilters: (filters: NebulaContext | undefined) => void;
|
||||
formBodyClassName?: string;
|
||||
formActionContainerClassName?: string;
|
||||
modal:
|
||||
| {
|
||||
close: () => void;
|
||||
}
|
||||
| undefined;
|
||||
}) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
chainIds: props.contextFilters?.chainIds
|
||||
? props.contextFilters.chainIds.join(",")
|
||||
: "",
|
||||
contractAddresses: props.contextFilters?.contractAddresses
|
||||
? props.contextFilters.contractAddresses.join(",")
|
||||
: "",
|
||||
walletAddresses: props.contextFilters?.walletAddresses
|
||||
? props.contextFilters.walletAddresses.join(",")
|
||||
: "",
|
||||
walletAddress: props.contextFilters?.walletAddress || "",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const { chainIds, contractAddresses, walletAddresses } = values;
|
||||
const { chainIds, walletAddress } = values;
|
||||
|
||||
const chainIdsArray = chainIds.split(",").filter((id) => id.trim());
|
||||
|
||||
const contractAddressesArray = contractAddresses
|
||||
.split(",")
|
||||
.filter((v) => v.trim());
|
||||
|
||||
const walletAddressesArray = walletAddresses
|
||||
.split(",")
|
||||
.filter((v) => v.trim());
|
||||
|
||||
props.updateFilters({
|
||||
const data = {
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
walletAddresses: walletAddressesArray,
|
||||
walletAddress: walletAddress || null,
|
||||
};
|
||||
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: "Context updated",
|
||||
error: "Failed to update context",
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
props.modal?.close?.();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context information to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-5 px-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={cn("flex w-full grow flex-col")}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-5 px-6 pb-6",
|
||||
props.formBodyClassName,
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chainIds"
|
||||
@@ -200,13 +189,14 @@ function ContextFilterDialogContent(props: {
|
||||
<FormLabel>Chain IDs</FormLabel>
|
||||
<FormControl>
|
||||
<MultiNetworkSelector
|
||||
disableChainId
|
||||
className="bg-background"
|
||||
selectedChainIds={form
|
||||
.watch()
|
||||
.chainIds.split(",")
|
||||
.filter(Boolean)
|
||||
.map(Number)}
|
||||
onChange={(values) => {
|
||||
console.log("values", values);
|
||||
form.setValue("chainIds", values.join(","));
|
||||
}}
|
||||
/>
|
||||
@@ -218,54 +208,44 @@ function ContextFilterDialogContent(props: {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contractAddresses"
|
||||
name="walletAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Contract Addresses</FormLabel>
|
||||
<FormLabel>Wallet Address</FormLabel>
|
||||
<FormControl>
|
||||
<AutoResizeTextarea
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
placeholder="0x123.."
|
||||
className="md:text-xs md:placeholder:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of contract addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="walletAddresses"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wallet Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of wallet addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-end gap-3 border-t bg-card p-6">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button className="min-w-28 gap-2" type="submit">
|
||||
{props.isPending && <Spinner className="size-4" />}
|
||||
Update
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-end gap-3 border-t bg-card p-6",
|
||||
props.formActionContainerClassName,
|
||||
)}
|
||||
>
|
||||
{props.modal && (
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-24 gap-2"
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
{updateMutation.isPending && <Spinner className="size-4" />}
|
||||
Update Context
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
sendMessage: (message: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="py-10 lg:py-16">
|
||||
<div className="relative py-10">
|
||||
<FancyBorders />
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
|
||||
@@ -61,7 +61,7 @@ export function NebulaAccountButton(props: {
|
||||
<div className="shrink-0">
|
||||
<GradientAvatar
|
||||
id={props.account?.id || "default"}
|
||||
src={""}
|
||||
src={props.account?.image || ""}
|
||||
className="size-9 rounded-lg"
|
||||
client={client}
|
||||
/>
|
||||
@@ -88,7 +88,7 @@ export function NebulaAccountButton(props: {
|
||||
<div className="shrink-0">
|
||||
<GradientAvatar
|
||||
id={props.account?.id || "default"}
|
||||
src={""}
|
||||
src={props.account?.image || ""}
|
||||
className="size-9 rounded-full"
|
||||
client={client}
|
||||
/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import {
|
||||
ThirdwebProvider,
|
||||
useActiveAccount,
|
||||
@@ -31,6 +32,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
|
||||
enableSystem={false}
|
||||
defaultTheme="dark"
|
||||
>
|
||||
<ToasterSetup />
|
||||
<SanctionedAddressesChecker>
|
||||
{props.children}
|
||||
</SanctionedAddressesChecker>
|
||||
@@ -40,6 +42,11 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ToasterSetup() {
|
||||
const { theme } = useTheme();
|
||||
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
|
||||
}
|
||||
|
||||
function SyncChainDefinitionsToConnectionManager() {
|
||||
const { allChainsV5 } = useAllChainsData();
|
||||
const connectionManager = useConnectionManager();
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function TotalSponsoredChartCardUI({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title={title || "Total Sponsored"}
|
||||
title={title || "Gas Sponsored"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
|
||||
import type { TransactionStats } from "types/analytics";
|
||||
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
|
||||
import { BarChart } from "../../../components/Analytics/BarChart";
|
||||
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
|
||||
|
||||
export async function TransactionsChartCardUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
className,
|
||||
onlyMainnet,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
className?: string;
|
||||
onlyMainnet?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const uniqueChainIds = [
|
||||
...new Set(data.map((item) => item.chainId).filter(Boolean)),
|
||||
];
|
||||
const chains = await Promise.all(
|
||||
uniqueChainIds.map((chainId) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(Number(chainId))).catch(() => undefined),
|
||||
),
|
||||
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
|
||||
|
||||
// Process data to combine by date and chain type
|
||||
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
|
||||
for (const item of data) {
|
||||
const chain = chains.find((c) => c.chainId === Number(item.chainId));
|
||||
|
||||
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
|
||||
if (chain?.testnet) {
|
||||
existing.testnet += item.count;
|
||||
} else {
|
||||
existing.mainnet += item.count;
|
||||
}
|
||||
dateMap.set(item.date, existing);
|
||||
}
|
||||
|
||||
// Convert to array and sort by date
|
||||
const timeSeriesData = Array.from(dateMap.entries())
|
||||
.map(([date, values]) => ({
|
||||
date,
|
||||
mainnet: values.mainnet,
|
||||
testnet: values.testnet,
|
||||
total: values.mainnet + values.testnet,
|
||||
}))
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
|
||||
const processedAggregatedData = {
|
||||
mainnet: aggregatedData
|
||||
.filter(
|
||||
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
testnet: aggregatedData
|
||||
.filter(
|
||||
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
total: aggregatedData.reduce((acc, curr) => acc + curr.count, 0),
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
mainnet: {
|
||||
label: "Mainnet Chains",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
testnet: {
|
||||
label: "Testnet Chains",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
total: {
|
||||
label: "All Chains",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
};
|
||||
|
||||
if (onlyMainnet) {
|
||||
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
|
||||
return (
|
||||
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
{title || "Transactions"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground"> {description}</p>
|
||||
<BarChart
|
||||
chartConfig={chartConfig}
|
||||
data={filteredData}
|
||||
activeKey="mainnet"
|
||||
emptyChartContent={<EmptyAccountAbstractionChartContent />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
title={title || "Transactions"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
(searchParams?.client_transactions as keyof typeof chartConfig) ??
|
||||
"mainnet"
|
||||
}
|
||||
queryKey="client_transactions"
|
||||
existingQueryParams={searchParams}
|
||||
aggregateFn={(_data, key) => processedAggregatedData[key]}
|
||||
className={className}
|
||||
// Get the trend from the last two COMPLETE periods
|
||||
trendFn={(data, key) =>
|
||||
data.filter((d) => (d[key] as number) > 0).length >= 3
|
||||
? ((data[data.length - 2]?.[key] as number) ?? 0) /
|
||||
((data[data.length - 3]?.[key] as number) ?? 0) -
|
||||
1
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getWalletConnections } from "@/api/analytics";
|
||||
import { type Project, getProjects } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { Changelog } from "components/dashboard/Changelog";
|
||||
import { subDays } from "date-fns";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TeamProjectsPage } from "./~/projects/TeamProjectsPage";
|
||||
import {
|
||||
type ProjectWithAnalytics,
|
||||
TeamProjectsPage,
|
||||
} from "./~/projects/TeamProjectsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string }>;
|
||||
@@ -15,12 +20,13 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const projects = await getProjects(params.team_slug);
|
||||
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
|
||||
<div className="flex grow flex-col">
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
|
||||
<TeamProjectsPage projects={projects} team={team} />
|
||||
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[320px]">
|
||||
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
@@ -31,3 +37,38 @@ export default async function Page(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProjectsWithAnalytics(
|
||||
projects: Project[],
|
||||
): Promise<Array<ProjectWithAnalytics>> {
|
||||
return Promise.all(
|
||||
projects.map(async (p) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = subDays(today, 30);
|
||||
|
||||
const data = await getWalletConnections({
|
||||
clientId: p.publishableKey,
|
||||
period: "all",
|
||||
from: thirtyDaysAgo,
|
||||
to: today,
|
||||
});
|
||||
|
||||
let uniqueWalletsConnected = 0;
|
||||
for (const d of data) {
|
||||
uniqueWalletsConnected += d.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: uniqueWalletsConnected,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: 0,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
getClientTransactions,
|
||||
getInAppWalletUsage,
|
||||
getUserOpUsage,
|
||||
getWalletConnections,
|
||||
@@ -31,9 +32,10 @@ import { getValidAccount } from "app/account/settings/getAccount";
|
||||
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
|
||||
import { Suspense } from "react";
|
||||
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
|
||||
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
|
||||
|
||||
// revalidate every 5 minutes
|
||||
export const revalidate = 300;
|
||||
export const maxDuration = 300;
|
||||
|
||||
type SearchParams = {
|
||||
usersChart?: string;
|
||||
@@ -100,6 +102,8 @@ async function OverviewPageContent(props: {
|
||||
inAppWalletUsage,
|
||||
userOpUsageTimeSeries,
|
||||
userOpUsage,
|
||||
clientTransactionsTimeSeries,
|
||||
clientTransactions,
|
||||
] = await Promise.all([
|
||||
// Aggregated wallet connections
|
||||
getWalletConnections({
|
||||
@@ -135,6 +139,19 @@ async function OverviewPageContent(props: {
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
// Client transactions
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}),
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
const isEmpty =
|
||||
@@ -180,6 +197,14 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{clientTransactions.length > 0 && (
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={clientTransactionsTimeSeries}
|
||||
aggregatedData={clientTransactions}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
)}
|
||||
{userOpUsage.length > 0 ? (
|
||||
<TotalSponsoredChartCardUI
|
||||
searchParams={searchParams}
|
||||
@@ -189,7 +214,7 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateCard
|
||||
metric="Sponsored Transactions"
|
||||
metric="Gas Sponsored"
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
|
||||
+68
-30
@@ -5,6 +5,9 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import { FormItem } from "@/components/ui/form";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { engineKeys } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
type BackendWallet,
|
||||
useEngineBackendWalletBalance,
|
||||
@@ -29,28 +32,31 @@ import {
|
||||
type UseDisclosureReturn,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { type ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { EngineBackendWalletOptions } from "lib/engine";
|
||||
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import {
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
PencilIcon,
|
||||
RefreshCcwIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
UploadIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import QRCode from "qrcode";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import invariant from "tiny-invariant";
|
||||
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { FormHelperText, FormLabel, Text } from "tw-components";
|
||||
import { prettyPrintCurrency } from "./utils";
|
||||
|
||||
interface BackendWalletsTableProps {
|
||||
@@ -59,6 +65,7 @@ interface BackendWalletsTableProps {
|
||||
isPending: boolean;
|
||||
isFetched: boolean;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
interface BackendWalletDashboard extends BackendWallet {
|
||||
@@ -72,48 +79,49 @@ interface BackendWalletBalanceCellProps {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
});
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
if (!chain || !backendWalletBalance) {
|
||||
return;
|
||||
const { data: backendWalletBalance, isFetching } =
|
||||
useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
});
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
|
||||
if (!backendWalletBalance || isFetching) {
|
||||
return <Skeleton className="h-6 w-36 rounded-lg" />;
|
||||
}
|
||||
|
||||
const balanceDisplay = prettyPrintCurrency({
|
||||
amount: backendWalletBalance.displayValue,
|
||||
symbol: backendWalletBalance.symbol,
|
||||
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
|
||||
});
|
||||
|
||||
const balanceComponent = (
|
||||
<Text fontWeight={backendWalletBalance.value === "0" ? "light" : "bold"}>
|
||||
{balanceDisplay}
|
||||
</Text>
|
||||
);
|
||||
const balanceComponent = <span>{balanceDisplay}</span>;
|
||||
|
||||
const explorer = chain?.explorers?.[0];
|
||||
const explorer = chain.blockExplorers?.[0];
|
||||
if (!explorer) {
|
||||
return balanceComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
variant="ghost"
|
||||
isExternal
|
||||
size="xs"
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorer.url}/address/${address}`}
|
||||
className="inline-flex items-center gap-2.5 rounded-lg px-2 py-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
{balanceComponent}
|
||||
</LinkButton>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,11 +131,13 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
isPending,
|
||||
isFetched,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const editDisclosure = useDisclosure();
|
||||
const receiveDisclosure = useDisclosure();
|
||||
const sendDisclosure = useDisclosure();
|
||||
const deleteDisclosure = useDisclosure();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
@@ -155,7 +165,31 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("address", {
|
||||
header: "Balance",
|
||||
header: () => (
|
||||
<div className="flex w-[180px] items-center gap-1.5">
|
||||
Balance
|
||||
<ToolTipLabel
|
||||
label="Refresh Balance"
|
||||
contentClassName="capitalize font-normal tracking-normal leading-normal"
|
||||
>
|
||||
<Button
|
||||
className="z-20 h-auto p-1.5 [&[data-pending='true']_svg]:animate-spin"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async (e) => {
|
||||
const buttonEl = e.currentTarget;
|
||||
buttonEl.setAttribute("data-pending", "true");
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWalletBalanceAll(),
|
||||
});
|
||||
buttonEl.setAttribute("data-pending", "false");
|
||||
}}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
</div>
|
||||
),
|
||||
cell: (cell) => {
|
||||
const address = cell.getValue();
|
||||
return (
|
||||
@@ -163,13 +197,14 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
instanceUrl={instanceUrl}
|
||||
address={address}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
id: "balance",
|
||||
}),
|
||||
];
|
||||
}, [instanceUrl, authToken]);
|
||||
}, [instanceUrl, authToken, chainId, queryClient]);
|
||||
|
||||
const [selectedBackendWallet, setSelectedBackendWallet] =
|
||||
useState<BackendWallet>();
|
||||
@@ -242,6 +277,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
disclosure={sendDisclosure}
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
)}
|
||||
{selectedBackendWallet && deleteDisclosure.isOpen && (
|
||||
@@ -417,24 +453,28 @@ const SendFundsModal = ({
|
||||
disclosure,
|
||||
instanceUrl,
|
||||
authToken,
|
||||
chainId,
|
||||
}: {
|
||||
fromWallet: BackendWallet;
|
||||
backendWallets: BackendWallet[];
|
||||
disclosure: UseDisclosureReturn;
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) => {
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
const form = useForm<SendFundsInput>();
|
||||
const sendTokens = useEngineSendTokens({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
const { idToChain } = useAllChainsData();
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl,
|
||||
address: fromWallet.address,
|
||||
authToken,
|
||||
chainId: chainId,
|
||||
});
|
||||
const chain = idToChain.get(chainId);
|
||||
const toWalletDisclosure = useDisclosure();
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
@@ -442,11 +482,9 @@ const SendFundsModal = ({
|
||||
}
|
||||
|
||||
const onSubmit = async (data: SendFundsInput) => {
|
||||
invariant(chain, "chain is required");
|
||||
|
||||
const promise = sendTokens.mutateAsync(
|
||||
{
|
||||
chainId: chain.chainId,
|
||||
chainId: chainId,
|
||||
fromAddress: fromWallet.address,
|
||||
toAddress: data.toAddress,
|
||||
amount: data.amount,
|
||||
|
||||
+15
-3
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineBackendWallets,
|
||||
useEngineWalletConfig,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { BackendWalletsTable } from "./backend-wallets-table";
|
||||
import { CreateBackendWalletButton } from "./create-backend-wallet-button";
|
||||
import { ImportBackendWalletButton } from "./import-backend-wallet-button";
|
||||
@@ -43,6 +45,10 @@ function BackendWalletsSection(props: {
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instance, teamSlug, authToken } = props;
|
||||
const activeWalletChain = useActiveWalletChain();
|
||||
const [_chainId, setChainId] = useState<number>();
|
||||
const chainId = _chainId || activeWalletChain?.id || 1;
|
||||
|
||||
const backendWallets = useEngineBackendWallets({
|
||||
instanceUrl: instance.url,
|
||||
authToken,
|
||||
@@ -105,9 +111,14 @@ function BackendWalletsSection(props: {
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Show balance for</span>
|
||||
{/* TODO - Replace with simple network selector - there's no need for user to switch chain */}
|
||||
<div className="flex flex-row">
|
||||
<NetworkSelectorButton />
|
||||
<SingleNetworkSelector
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
className="min-w-40 max-w-52 lg:max-w-60"
|
||||
popoverContentClassName="!w-[80vw] md:!w-[500px]"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,6 +131,7 @@ function BackendWalletsSection(props: {
|
||||
isPending={backendWallets.isPending}
|
||||
isFetched={backendWallets.isFetched}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ export const prettyPrintCurrency = ({
|
||||
symbol,
|
||||
}: {
|
||||
amount?: string | number;
|
||||
symbol: string;
|
||||
symbol?: string;
|
||||
}): string => {
|
||||
const amountNumber =
|
||||
typeof amount === "string" ? Number.parseFloat(amount) : (amount ?? 0);
|
||||
return `${amountNumber.toFixed(6)} ${symbol}`;
|
||||
return `${amountNumber.toFixed(6)} ${symbol || ""}`;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Team } from "@/api/team";
|
||||
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
|
||||
import { CopyButton } from "@/components/ui/CopyButton";
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -11,6 +12,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,51 +25,90 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type SortById = "name" | "createdAt";
|
||||
type SortById = "name" | "createdAt" | "monthlyActiveUsers";
|
||||
|
||||
export type ProjectWithAnalytics = Project & {
|
||||
monthlyActiveUsers: number;
|
||||
};
|
||||
|
||||
export function TeamProjectsPage(props: {
|
||||
projects: Project[];
|
||||
projects: ProjectWithAnalytics[];
|
||||
team: Team;
|
||||
}) {
|
||||
const { projects } = props;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortBy, setSortBy] = useState<SortById>("createdAt");
|
||||
const [sortBy, setSortBy] = useState<SortById>("monthlyActiveUsers");
|
||||
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
|
||||
useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
let projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
const sortedProjects = useMemo(() => {
|
||||
let _projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
if (sortBy === "name") {
|
||||
projectsToShow = projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
projectsToShow = projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
}
|
||||
if (sortBy === "name") {
|
||||
_projectsToShow = _projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
} else if (sortBy === "monthlyActiveUsers") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) => b.monthlyActiveUsers - a.monthlyActiveUsers,
|
||||
);
|
||||
}
|
||||
|
||||
return _projectsToShow;
|
||||
}, [searchTerm, sortBy, projects]);
|
||||
|
||||
const pageSize = 8;
|
||||
const [page, setPage] = useState(1);
|
||||
const paginatedProjects = sortedProjects.slice(
|
||||
(page - 1) * pageSize,
|
||||
page * pageSize,
|
||||
);
|
||||
|
||||
const showPagination = sortedProjects.length > pageSize;
|
||||
const totalPages = Math.ceil(sortedProjects.length / pageSize);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
{/* Filters + Add New */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<SearchInput value={searchTerm} onValueChange={setSearchTerm} />
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onValueChange={(v) => {
|
||||
setSearchTerm(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<SelectBy value={sortBy} onChange={setSortBy} />
|
||||
<SelectBy
|
||||
value={sortBy}
|
||||
onChange={(v) => {
|
||||
setSortBy(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<AddNewButton
|
||||
createProject={() => setIsCreateProjectDialogOpen(true)}
|
||||
teamMembersSettingsPath={`/team/${props.team.slug}/~/settings/members`}
|
||||
@@ -74,23 +119,33 @@ export function TeamProjectsPage(props: {
|
||||
<div className="h-6" />
|
||||
|
||||
{/* Projects */}
|
||||
{projectsToShow.length === 0 ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{paginatedProjects.length === 0 ? (
|
||||
<>
|
||||
{searchTerm !== "" ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects found</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{projectsToShow.map((project) => {
|
||||
{paginatedProjects.map((project) => {
|
||||
return (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
@@ -102,9 +157,20 @@ export function TeamProjectsPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPagination && (
|
||||
<div className="py-6">
|
||||
<PaginationButtons
|
||||
activePage={page}
|
||||
onPageClick={setPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={isCreateProjectDialogOpen}
|
||||
onOpenChange={setIsCreateProjectDialogOpen}
|
||||
teamSlug={props.team.slug}
|
||||
onCreateAndComplete={() => {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
@@ -118,7 +184,7 @@ export function TeamProjectsPage(props: {
|
||||
}
|
||||
|
||||
function ProjectCard(props: {
|
||||
project: Project;
|
||||
project: ProjectWithAnalytics;
|
||||
team_slug: string;
|
||||
}) {
|
||||
const { project, team_slug } = props;
|
||||
@@ -130,34 +196,51 @@ function ProjectCard(props: {
|
||||
{/* TODO - set image */}
|
||||
<ProjectAvatar className="size-10 rounded-full" src="" />
|
||||
|
||||
<div className="flex-grow flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
<CopyButton
|
||||
text={project.publishableKey}
|
||||
iconClassName="z-10 size-3"
|
||||
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-1">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
|
||||
<p className="flex items-center text-muted-foreground text-sm">
|
||||
{truncate(project.publishableKey, 32)}
|
||||
<p className="flex items-center gap-1 text-muted-foreground text-sm">
|
||||
<span>{project.monthlyActiveUsers}</span>
|
||||
Monthly Active Users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
|
||||
<EllipsisVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[180px] p-1">
|
||||
<CopyTextButton
|
||||
textToCopy={project.publishableKey}
|
||||
textToShow="Copy Client ID"
|
||||
copyIconPosition="right"
|
||||
tooltip={undefined}
|
||||
variant="ghost"
|
||||
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-3"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function truncate(str: string, stringLimit: number) {
|
||||
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
|
||||
}
|
||||
|
||||
function SearchInput(props: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
@@ -209,10 +292,11 @@ function SelectBy(props: {
|
||||
value: SortById;
|
||||
onChange: (value: SortById) => void;
|
||||
}) {
|
||||
const values: SortById[] = ["name", "createdAt"];
|
||||
const values: SortById[] = ["name", "createdAt", "monthlyActiveUsers"];
|
||||
const valueToLabel: Record<SortById, string> = {
|
||||
name: "Name",
|
||||
createdAt: "Creation Date",
|
||||
monthlyActiveUsers: "Monthly Active Users",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -223,7 +307,12 @@ function SelectBy(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] bg-card capitalize">
|
||||
Sort by {valueToLabel[props.value]}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="!hidden lg:!inline text-muted-foreground">
|
||||
Sort by
|
||||
</span>
|
||||
{valueToLabel[props.value]}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{values.map((value) => (
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { getTeamSettingsLinks } from "./getTeamSettingsLinks";
|
||||
|
||||
export function TeamSettingsSidebar(props: {
|
||||
team: Team;
|
||||
account: Pick<Account, "id"> | undefined;
|
||||
account: Pick<Account, "id" | "image"> | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const teamLinks = getTeamSettingsLinks(props.team.slug);
|
||||
@@ -34,7 +34,7 @@ export function TeamSettingsSidebar(props: {
|
||||
team={props.team}
|
||||
titleAvatarIcon={{
|
||||
id: props.account?.id,
|
||||
src: "", // TODO - set account image
|
||||
src: props.account?.image || "",
|
||||
}}
|
||||
client={props.client}
|
||||
/>
|
||||
@@ -60,7 +60,7 @@ function RenderLinkGroup(props: {
|
||||
<div className="flex items-center gap-1.5 px-4">
|
||||
<GradientAvatar
|
||||
src={props.titleAvatarIcon.src}
|
||||
className="size-4"
|
||||
className="size-[18px]"
|
||||
id={props.titleAvatarIcon.id}
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
+4
-6
@@ -1,24 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import type { Team } from "@/api/team";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { upload } from "thirdweb/storage";
|
||||
import { TeamGeneralSettingsPageUI } from "./TeamGeneralSettingsPageUI";
|
||||
import { updateTeam } from "./updateTeam";
|
||||
|
||||
export function TeamGeneralSettingsPage(props: {
|
||||
team: Team;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const client = useThirdwebClient();
|
||||
|
||||
return (
|
||||
<TeamGeneralSettingsPageUI
|
||||
team={props.team}
|
||||
client={client}
|
||||
client={props.client}
|
||||
updateTeamField={async (teamValue) => {
|
||||
await updateTeam({
|
||||
teamId: props.team.id,
|
||||
@@ -38,7 +36,7 @@ export function TeamGeneralSettingsPage(props: {
|
||||
if (file) {
|
||||
// upload to IPFS
|
||||
uri = await upload({
|
||||
client: getThirdwebClient(props.authToken),
|
||||
client: props.client,
|
||||
files: [file],
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ function TeamAvatarFormControl(props: {
|
||||
disabled: false,
|
||||
isPending: updateTeamAvatarMutation.isPending,
|
||||
}}
|
||||
noPermissionText={undefined} // TODO
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<div className="flex flex-row gap-4 md:justify-between">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { TeamGeneralSettingsPage } from "./general/TeamGeneralSettingsPage";
|
||||
@@ -14,5 +15,7 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <TeamGeneralSettingsPage team={team} authToken={token} />;
|
||||
return (
|
||||
<TeamGeneralSettingsPage team={team} client={getThirdwebClient(token)} />
|
||||
);
|
||||
}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { defineChain, getContract } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard";
|
||||
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
|
||||
import { fetchDashboardContractMetadata } from "../../../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import type { TransactionStats } from "../../../../../../types/analytics";
|
||||
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
|
||||
|
||||
export function TransactionsChartsUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
<ChainDistributionCard data={aggregatedData} />
|
||||
<ContractDistributionCard data={aggregatedData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.chainId] = (acc[curr.chainId] || 0) + curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(key));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
return {
|
||||
label: chainMeta?.slug || chain.id.toString(),
|
||||
value,
|
||||
link: `/${chain.id}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () => new Set(data.map((d) => `${d.chainId}`)).size;
|
||||
|
||||
return (
|
||||
<PieChartCard title="Chains" data={reducedData} aggregateFn={aggregateFn} />
|
||||
);
|
||||
}
|
||||
|
||||
async function ContractDistributionCard({
|
||||
data,
|
||||
}: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[`${curr.chainId}:${curr.contractAddress}`] =
|
||||
(acc[`${curr.chainId}:${curr.contractAddress}`] || 0) +
|
||||
curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
const [chainId, contractAddress] = key.split(":");
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(chainId));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
const contractData = await fetchDashboardContractMetadata(
|
||||
getContract({
|
||||
chain,
|
||||
address: contractAddress as string, // we filter above
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
).catch(() => undefined);
|
||||
return {
|
||||
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
|
||||
link: `/${chainId}/${contractAddress}`,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () =>
|
||||
new Set(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.map((d) => `${d.chainId}:${d.contractAddress}`),
|
||||
).size;
|
||||
|
||||
return (
|
||||
<PieChartCard
|
||||
title="Contracts"
|
||||
data={reducedData}
|
||||
aggregateFn={aggregateFn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { LoadingChartState } from "components/analytics/empty-chart-state";
|
||||
import { Suspense } from "react";
|
||||
import type { AnalyticsQueryParams } from "types/analytics";
|
||||
import { getClientTransactions } from "../../../../../../@/api/analytics";
|
||||
import { TransactionsChartsUI } from "./TransactionCharts";
|
||||
|
||||
export function TransactionsCharts(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
return (
|
||||
// TODO: Add better LoadingChartState
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-[400px]">
|
||||
<LoadingChartState />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TransactionsChartCardAsync {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function TransactionsChartCardAsync(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
const [data, aggregatedData] = await Promise.all([
|
||||
getClientTransactions(props),
|
||||
getClientTransactions({
|
||||
...props,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!aggregatedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionsChartsUI
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
searchParams={props.searchParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+14
-1
@@ -1,5 +1,8 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getProject } from "../../../../../../@/api/projects";
|
||||
import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -27,9 +30,19 @@ export default async function Page(props: {
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const apiKey = await getAPIKeyForProjectId(project.id);
|
||||
if (!apiKey) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.project_slug}
|
||||
clientId={apiKey.key}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChar
|
||||
import { EmptyState } from "../../components/Analytics/EmptyState";
|
||||
import { PieChartCard } from "../../components/Analytics/PieChartCard";
|
||||
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsCharts } from "./components/Transactions";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
@@ -49,6 +50,8 @@ interface PageProps {
|
||||
searchParams: Promise<PageSearchParams>;
|
||||
}
|
||||
|
||||
export const maxDuration = 300;
|
||||
|
||||
export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
@@ -174,12 +177,6 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/connect/quickstart"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
{walletConnections.status === "fulfilled" &&
|
||||
walletConnections.value.length > 0 ? (
|
||||
@@ -200,6 +197,13 @@ async function ProjectAnalytics(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<TransactionsCharts
|
||||
searchParams={searchParams}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
{userOpUsageTimeSeries.status === "fulfilled" &&
|
||||
userOpUsage.status === "fulfilled" &&
|
||||
userOpUsage.value.length > 0 ? (
|
||||
@@ -216,6 +220,12 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -417,7 +427,7 @@ async function TotalSponsoredCard({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title="Total Sponsored"
|
||||
title="Gas Sponsored"
|
||||
chartConfig={chartConfig}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
data={timeSeriesData}
|
||||
|
||||
+10
@@ -60,6 +60,16 @@ function Story() {
|
||||
payConfig: "/payConfig",
|
||||
}}
|
||||
onKeyUpdated={undefined}
|
||||
rotateSecretKey={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
data: {
|
||||
secret: new Array(86).fill("x").join(""),
|
||||
secretHash: new Array(64).fill("x").join(""),
|
||||
secretMasked: "123...4567",
|
||||
},
|
||||
};
|
||||
}}
|
||||
showNebulaSettings={false}
|
||||
/>
|
||||
|
||||
|
||||
+275
-6
@@ -1,12 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
|
||||
import { SettingsCard } from "@/components/blocks/SettingsCard";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -14,13 +23,14 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
useRevokeApiKey,
|
||||
useUpdateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import {
|
||||
type ServiceName,
|
||||
@@ -28,8 +38,13 @@ import {
|
||||
} from "@thirdweb-dev/service-utils";
|
||||
import { format } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
RefreshCcwIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,11 +62,20 @@ type EditProjectUIPaths = {
|
||||
afterDeleteRedirectTo: string;
|
||||
};
|
||||
|
||||
type RotateSecretKeyAPIReturnType = {
|
||||
data: {
|
||||
secret: string;
|
||||
secretMasked: string;
|
||||
secretHash: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function ProjectGeneralSettingsPage(props: {
|
||||
apiKey: ApiKey;
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
projectId: string;
|
||||
}) {
|
||||
const updateMutation = useUpdateApiKey();
|
||||
const deleteMutation = useRevokeApiKey();
|
||||
@@ -64,6 +88,24 @@ export function ProjectGeneralSettingsPage(props: {
|
||||
paths={props.paths}
|
||||
onKeyUpdated={props.onKeyUpdated}
|
||||
showNebulaSettings={props.showNebulaSettings}
|
||||
rotateSecretKey={async () => {
|
||||
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
|
||||
pathname: "/v2/keys/rotate-secret-key",
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectId: props.projectId,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,6 +126,7 @@ interface EditApiKeyProps {
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
}
|
||||
|
||||
type UpdateAPIForm = UseFormReturn<ProjectSettingsPageFormSchema>;
|
||||
@@ -216,7 +259,10 @@ export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
<APIKeyDetails apiKey={apiKey} />
|
||||
<APIKeyDetails
|
||||
apiKey={apiKey}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
/>
|
||||
|
||||
<AllowedDomainsSetting
|
||||
form={form}
|
||||
@@ -609,10 +655,13 @@ function EnabledServicesSetting(props: {
|
||||
|
||||
function APIKeyDetails({
|
||||
apiKey,
|
||||
rotateSecretKey,
|
||||
}: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
apiKey: ApiKey;
|
||||
}) {
|
||||
const { createdAt, updatedAt, lastAccessedAt } = apiKey;
|
||||
const [secretKeyMasked, setSecretKeyMasked] = useState(apiKey.secretMasked);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 rounded-lg border border-border bg-card px-4 py-6 lg:px-6">
|
||||
@@ -632,7 +681,7 @@ function APIKeyDetails({
|
||||
</div>
|
||||
|
||||
{/* NOTE: for very old api keys the secret might be `null`, if that's the case we skip it */}
|
||||
{apiKey.secretMasked && (
|
||||
{secretKeyMasked && (
|
||||
<div>
|
||||
<h3>Secret Key</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
@@ -641,8 +690,17 @@ function APIKeyDetails({
|
||||
the time of creation for the full secret key.
|
||||
</p>
|
||||
|
||||
<div className="max-w-[350px] rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm">
|
||||
{apiKey.secretMasked}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
|
||||
{secretKeyMasked}
|
||||
</div>
|
||||
|
||||
<RotateSecretKeyButton
|
||||
rotateSecretKey={rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
setSecretKeyMasked(data.data.secretMasked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -726,3 +784,214 @@ function DeleteProject(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RotateSecretKeyButton(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
if (!isModalCloseAllowed) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(v);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent
|
||||
className="overflow-hidden p-0"
|
||||
dialogCloseClassName={cn(!isModalCloseAllowed && "hidden")}
|
||||
>
|
||||
<RotateSecretKeyModalContent
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
closeModal={() => {
|
||||
setIsOpen(false);
|
||||
setIsModalCloseAllowed(true);
|
||||
}}
|
||||
disableModalClose={() => setIsModalCloseAllowed(false)}
|
||||
onSuccess={props.onSuccess}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
type RotateSecretKeyScreen =
|
||||
| { id: "initial" }
|
||||
| { id: "save-newkey"; secretKey: string };
|
||||
|
||||
function RotateSecretKeyModalContent(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
closeModal: () => void;
|
||||
disableModalClose: () => void;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
|
||||
id: "initial",
|
||||
});
|
||||
|
||||
if (screen.id === "save-newkey") {
|
||||
return (
|
||||
<SaveNewKeyScreen
|
||||
secretKey={screen.secretKey}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "initial") {
|
||||
return (
|
||||
<RotateSecretKeyInitialScreen
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
props.disableModalClose();
|
||||
props.onSuccess(data);
|
||||
setScreen({ id: "save-newkey", secretKey: data.data.secret });
|
||||
}}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function RotateSecretKeyInitialScreen(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||
const rotateKeyMutation = useMutation({
|
||||
mutationFn: props.rotateSecretKey,
|
||||
onSuccess: (data) => {
|
||||
props.onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
toast.error("Failed to rotate secret key");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rotate Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Current secret key will stop working</AlertTitle>
|
||||
<AlertDescription>
|
||||
Rotating the secret key will invalidate the current secret key and
|
||||
generate a new one. This action is irreversible.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isConfirmed}
|
||||
onCheckedChange={(v) => setIsConfirmed(!!v)}
|
||||
/>
|
||||
I understand the consequences of rotating the secret key
|
||||
</CheckboxWithLabel>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button variant="outline" onClick={props.closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
disabled={!isConfirmed || rotateKeyMutation.isPending}
|
||||
onClick={() => {
|
||||
rotateKeyMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{rotateKeyMutation.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
)}
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveNewKeyScreen(props: {
|
||||
secretKey: string;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isSecretStored, setIsSecretStored] = useState(false);
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save New Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<CopyTextButton
|
||||
textToCopy={props.secretKey}
|
||||
className="!h-auto w-full justify-between bg-card px-3 py-3 font-mono"
|
||||
textToShow={props.secretKey}
|
||||
copyIconPosition="right"
|
||||
tooltip="Copy Secret Key"
|
||||
/>
|
||||
<div className="h-4" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Do not share or expose your secret key</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mb-5">
|
||||
Secret keys cannot be recovered. If you lose your secret key, you
|
||||
will need to rotate the secret key or create a new Project.
|
||||
</div>
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isSecretStored}
|
||||
onCheckedChange={(v) => {
|
||||
setIsSecretStored(!!v);
|
||||
}}
|
||||
/>
|
||||
I confirm that I've securely stored my secret key
|
||||
</CheckboxWithLabel>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!isSecretStored}
|
||||
onClick={props.closeModal}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -9,9 +9,10 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
team: Team;
|
||||
project_slug: string;
|
||||
apiKey: ApiKey;
|
||||
projectId: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const { team, project_slug, apiKey } = props;
|
||||
const { team, project_slug, apiKey, projectId } = props;
|
||||
const projectLayout = `/team/${team.slug}/${project_slug}`;
|
||||
|
||||
// TODO - add a Project Image form field on this page
|
||||
@@ -19,6 +20,7 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
return (
|
||||
<ProjectGeneralSettingsPage
|
||||
apiKey={apiKey}
|
||||
projectId={projectId}
|
||||
paths={{
|
||||
aaConfig: `${projectLayout}/connect/account-abstraction/settings`,
|
||||
inAppConfig: `${projectLayout}/connect/in-app-wallets/settings`,
|
||||
|
||||
@@ -31,6 +31,7 @@ export default async function Page(props: {
|
||||
<ProjectGeneralSettingsPageForTeams
|
||||
apiKey={apiKey}
|
||||
project_slug={project_slug}
|
||||
projectId={project.id}
|
||||
team={team}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getTeamBySlug } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TWAutoConnect } from "../../components/autoconnect";
|
||||
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
|
||||
|
||||
export default async function RootTeamLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -19,6 +20,7 @@ export default async function RootTeamLayout(props: {
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
<TWAutoConnect />
|
||||
<AppFooter />
|
||||
<SaveLastVisitedTeamPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { PieChart } from "./PieChart";
|
||||
import { Stat } from "./Stat";
|
||||
|
||||
@@ -11,6 +12,7 @@ type ChartData = {
|
||||
value: number;
|
||||
label: string;
|
||||
fill?: string;
|
||||
link?: string;
|
||||
};
|
||||
export function PieChartCard({
|
||||
title,
|
||||
@@ -22,11 +24,11 @@ export function PieChartCard({
|
||||
aggregateFn?: (data: ChartData[]) => number;
|
||||
}) {
|
||||
const processedData = (() => {
|
||||
if (data.length <= 9) return data;
|
||||
|
||||
// Sort by value descending
|
||||
const sorted = [...data].sort((a, b) => b.value - a.value);
|
||||
|
||||
if (sorted.length <= 9) return sorted;
|
||||
|
||||
// Take top 9
|
||||
const top10 = sorted.slice(0, 9).map((item) => ({
|
||||
...item,
|
||||
@@ -62,13 +64,27 @@ export function PieChartCard({
|
||||
<CardFooter className="no-scrollbar flex max-w-full justify-center p-6 pt-0 max-md:overflow-x-auto">
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2.5">
|
||||
{processedData.map(
|
||||
({ label, fill }: { label: string; fill?: string }) => (
|
||||
({
|
||||
label,
|
||||
fill,
|
||||
link,
|
||||
}: { label: string; fill?: string; link?: string }) => (
|
||||
<div key={fill} className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-2 rounded-full"
|
||||
style={{ background: fill }}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
{link ? (
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="text-muted-foreground text-xs hover:text-link-foreground"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function TeamSelectionUI(props: {
|
||||
currentTeam: Team | undefined;
|
||||
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
|
||||
upgradeTeamLink: string | undefined;
|
||||
account: Pick<Account, "email" | "id"> | undefined;
|
||||
account: Pick<Account, "email" | "id" | "image"> | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { setHoveredTeam, currentTeam, teamsAndProjects } = props;
|
||||
@@ -52,9 +52,8 @@ export function TeamSelectionUI(props: {
|
||||
asChild
|
||||
>
|
||||
<Link href="/account">
|
||||
{/* TODO set Image src */}
|
||||
<GradientAvatar
|
||||
src={""}
|
||||
src={props.account?.image || ""}
|
||||
className="size-4"
|
||||
id={props.account?.id}
|
||||
client={props.client}
|
||||
|
||||
@@ -65,6 +65,11 @@ export function TeamHeaderLoggedIn(props: {
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={createProjectDialogState.isOpen}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={() =>
|
||||
setCreateProjectDialogState({
|
||||
isOpen: false,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { setCookie } from "../../../../lib/cookie";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./consts";
|
||||
|
||||
export function SaveLastVisitedTeamPage() {
|
||||
const pathname = usePathname();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setCookie(LAST_VISITED_TEAM_PAGE_PATH, pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Only keep constants here - this file is imported in middleware too
|
||||
|
||||
export const LAST_VISITED_TEAM_PAGE_PATH = "last-visited-team-page-path";
|
||||
+8
-6
@@ -25,7 +25,7 @@ import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { ZERO_ADDRESS, getContract } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import {
|
||||
deployContractfromDeployMetadata,
|
||||
@@ -567,11 +567,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
const contractAddr = await deployMutation.mutateAsync(formData);
|
||||
|
||||
// send verification request - no need to await
|
||||
verifyContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
verifyContract(
|
||||
getContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
}),
|
||||
);
|
||||
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user