Compare commits
108
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c645b42684 | ||
|
|
7583074c06 | ||
|
|
fbafb65f5c | ||
|
|
6b286c7e83 | ||
|
|
159ffbfaee | ||
|
|
0c1bb6ccae | ||
|
|
dceb47de8b | ||
|
|
dfbdbd06e5 | ||
|
|
66bbedbe3a | ||
|
|
2fbf105d15 | ||
|
|
afcaa91873 | ||
|
|
51f955352d | ||
|
|
6b2a90ac89 | ||
|
|
cf53184bb1 | ||
|
|
815b9b1ce1 | ||
|
|
ae1baa5105 | ||
|
|
92266b7459 | ||
|
|
80ad84b803 | ||
|
|
633f0cadf1 | ||
|
|
91b0367b23 | ||
|
|
c86e13b522 | ||
|
|
bc42c2fdfb | ||
|
|
cad9c74bef | ||
|
|
6f2f92ddea | ||
|
|
97c9ab7e1c | ||
|
|
b73fa4b16f | ||
|
|
cbfe9ec459 | ||
|
|
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 |
@@ -9,7 +9,8 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
"/connect/account-abstraction",
|
||||
"/connect/universal-bridge",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
|
||||
+12
-11
@@ -46,12 +46,12 @@
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.51.0",
|
||||
"@sentry/nextjs": "8.53.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.2",
|
||||
"@vercel/functions": "^1.6.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
@@ -74,7 +74,7 @@
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.1",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -83,17 +83,18 @@
|
||||
"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.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.2",
|
||||
"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:*",
|
||||
@@ -106,7 +107,7 @@
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
@@ -117,7 +118,7 @@
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -129,12 +130,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.18.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.43.5",
|
||||
"knip": "5.43.6",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export type Team = {
|
||||
image?: string;
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
supportPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
@@ -47,6 +48,10 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTeamById(id: string) {
|
||||
return getTeamBySlug(id);
|
||||
}
|
||||
|
||||
export async function getTeams() {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AsteriskIcon, InfoIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
|
||||
export function FormFieldSetup(props: {
|
||||
htmlFor?: string;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
errorMessage: React.ReactNode | undefined;
|
||||
children: React.ReactNode;
|
||||
tooltip?: React.ReactNode;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,6 +27,7 @@ export function DatePickerWithRange(props: {
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
labelOverride?: string;
|
||||
popoverAlign?: "start" | "end" | "center";
|
||||
}) {
|
||||
const [screen, setScreen] = React.useState<"from" | "to">("from");
|
||||
const { from, to, setFrom, setTo } = props;
|
||||
@@ -65,7 +66,11 @@ export function DatePickerWithRange(props: {
|
||||
</PopoverTrigger>
|
||||
|
||||
{/* Popover */}
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align={props.popoverAlign || "start"}
|
||||
sideOffset={10}
|
||||
>
|
||||
<DynamicHeight>
|
||||
<div>
|
||||
{!isValid && (
|
||||
|
||||
@@ -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) =>
|
||||
@@ -124,4 +125,6 @@ export const engineKeys = {
|
||||
[...engineKeys.all, engineId, "alerts"] as const,
|
||||
notificationChannels: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "notificationChannels"] as const,
|
||||
walletCredentials: (instance: string) =>
|
||||
[...engineKeys.all, instance, "walletCredentials"] as const,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -93,7 +93,8 @@ type EngineFeature =
|
||||
| "CONTRACT_SUBSCRIPTIONS"
|
||||
| "IP_ALLOWLIST"
|
||||
| "HETEROGENEOUS_WALLET_TYPES"
|
||||
| "SMART_BACKEND_WALLETS";
|
||||
| "SMART_BACKEND_WALLETS"
|
||||
| "WALLET_CREDENTIALS";
|
||||
|
||||
interface EngineSystemHealth {
|
||||
status: string;
|
||||
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
|
||||
gcpKmsKeyRingId: string;
|
||||
gcpApplicationCredentialEmail: string;
|
||||
gcpApplicationCredentialPrivateKey: string;
|
||||
}
|
||||
| {
|
||||
type: "circle";
|
||||
circleApiKey: string;
|
||||
};
|
||||
|
||||
export function useEngineSetWalletConfig(params: {
|
||||
@@ -869,8 +874,8 @@ export function useEngineSetWalletConfig(params: {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<WalletConfigResponse, void, SetWalletConfigInput>({
|
||||
mutationFn: async (input) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: SetWalletConfigInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}configuration/wallets`, {
|
||||
@@ -884,7 +889,7 @@ export function useEngineSetWalletConfig(params: {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result;
|
||||
return json.result as WalletConfigResponse;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateBackendWalletInput = {
|
||||
type: EngineBackendWalletType;
|
||||
label?: string;
|
||||
};
|
||||
export type CreateBackendWalletInput =
|
||||
| {
|
||||
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
|
||||
label?: string;
|
||||
}
|
||||
| {
|
||||
type: "circle" | "smart:circle";
|
||||
label?: string;
|
||||
credentialId: string;
|
||||
isTestnet: boolean;
|
||||
};
|
||||
|
||||
export function useEngineCreateBackendWallet(params: {
|
||||
instanceUrl: string;
|
||||
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface WalletCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
isDefault: boolean | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface CreateWalletCredentialInput {
|
||||
type: "circle";
|
||||
label: string;
|
||||
entitySecret?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export function useEngineWalletCredentials(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
},
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
return (json.result as WalletCredential[]) || [];
|
||||
},
|
||||
enabled: !!instanceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineCreateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateWalletCredentialInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials`, {
|
||||
method: "POST",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateWalletCredentialInput {
|
||||
label?: string;
|
||||
isDefault?: boolean;
|
||||
entitySecret?: string;
|
||||
}
|
||||
|
||||
export function useEngineUpdateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
...input
|
||||
}: UpdateWalletCredentialInput & { id: string }) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
|
||||
method: "PUT",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+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>
|
||||
);
|
||||
}
|
||||
|
||||
-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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-613
@@ -1,613 +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}
|
||||
teamSlug={undefined}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
+22
-10
@@ -1,6 +1,10 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
|
||||
import {
|
||||
fetchLatestPublishedContractVersion,
|
||||
fetchPublishedContractVersions,
|
||||
} from "components/contract-components/fetch-contracts-with-versions";
|
||||
import { isAddress } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { ModuleMeta } from "./install-module-params";
|
||||
@@ -14,16 +18,24 @@ export async function getModuleInstalledParams(ext: ModuleMeta) {
|
||||
client: getThirdwebClient(),
|
||||
name: ext.publisherAddress,
|
||||
});
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
// find the version we want
|
||||
const publishedModule =
|
||||
ext.moduleVersion === "latest"
|
||||
? allPublishedModules[0]
|
||||
: allPublishedModules.find((v) => v.version === ext.moduleVersion);
|
||||
let publishedModule: FetchDeployMetadataResult | undefined = undefined;
|
||||
|
||||
if (ext.moduleVersion === "latest") {
|
||||
publishedModule = await fetchLatestPublishedContractVersion(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
} else {
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
publishedModule = allPublishedModules.find(
|
||||
(v) => v.version === ext.moduleVersion,
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
publishedModule,
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export function ContractSettingsPageClient(props: {
|
||||
contract={props.contract}
|
||||
functionSelectors={metadataQuery.data.functionSelectors}
|
||||
twAccount={props.twAccount}
|
||||
hasDefaultFeeConfig={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-5
@@ -79,8 +79,9 @@ export function ContractSettingsPage(props: {
|
||||
contract: ThirdwebContract;
|
||||
functionSelectors: string[];
|
||||
twAccount: Account | undefined;
|
||||
hasDefaultFeeConfig: boolean;
|
||||
}) {
|
||||
const { functionSelectors, contract, twAccount } = props;
|
||||
const { functionSelectors, contract, twAccount, hasDefaultFeeConfig } = props;
|
||||
return (
|
||||
<ContractSettingsPageInner
|
||||
contract={contract}
|
||||
@@ -96,10 +97,13 @@ export function ContractSettingsPage(props: {
|
||||
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPlatformFeesSupported={[
|
||||
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
|
||||
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPlatformFeesSupported={
|
||||
!hasDefaultFeeConfig &&
|
||||
[
|
||||
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
|
||||
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
|
||||
].every(Boolean)
|
||||
}
|
||||
twAccount={twAccount}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-4
@@ -132,9 +132,8 @@ export const SettingsPlatformFees = ({
|
||||
<SolidityInput
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("platform_fee_recipient")}
|
||||
isDisabled={!address || sendAndConfirmTx.isPending}
|
||||
disabled={!address || sendAndConfirmTx.isPending}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
@@ -155,7 +154,6 @@ export const SettingsPlatformFees = ({
|
||||
>
|
||||
<FormLabel>Percentage</FormLabel>
|
||||
<BasisPointsInput
|
||||
variant="filled"
|
||||
value={form.watch("platform_fee_basis_points")}
|
||||
onChange={(value) =>
|
||||
form.setValue("platform_fee_basis_points", value, {
|
||||
@@ -163,7 +161,7 @@ export const SettingsPlatformFees = ({
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
disabled={sendAndConfirmTx.isPending}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
|
||||
+1
-2
@@ -128,10 +128,9 @@ export const SettingsPrimarySale = ({
|
||||
>
|
||||
<FormLabel>Recipient Address</FormLabel>
|
||||
<SolidityInput
|
||||
isDisabled={mutation.isPending || !address}
|
||||
disabled={mutation.isPending || !address}
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("primary_sale_recipient")}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
|
||||
+1
-3
@@ -144,9 +144,8 @@ export const SettingsRoyalties = ({
|
||||
<SolidityInput
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("fee_recipient")}
|
||||
isDisabled={!address}
|
||||
disabled={!address}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
@@ -165,7 +164,6 @@ export const SettingsRoyalties = ({
|
||||
>
|
||||
<FormLabel>Percentage</FormLabel>
|
||||
<BasisPointsInput
|
||||
variant="filled"
|
||||
value={form.watch("seller_fee_basis_points")}
|
||||
onChange={(value) =>
|
||||
form.setValue("seller_fee_basis_points", value, {
|
||||
|
||||
+10
@@ -1,5 +1,7 @@
|
||||
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -33,11 +35,19 @@ export default async function Page(props: {
|
||||
getContractPageMetadata(info.contract),
|
||||
]);
|
||||
|
||||
let hasDefaultFeeConfig = true;
|
||||
try {
|
||||
const feeInfo = await getPlatformFeeInfo({ contract });
|
||||
hasDefaultFeeConfig =
|
||||
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<ContractSettingsPage
|
||||
contract={info.contract}
|
||||
functionSelectors={metadata.functionSelectors}
|
||||
twAccount={account}
|
||||
hasDefaultFeeConfig={hasDefaultFeeConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+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"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ContractPublishForm } from "components/contract-components/contract-pub
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { fetchDeployMetadata } from "thirdweb/contract";
|
||||
import { getPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
|
||||
import { getLatestPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
|
||||
|
||||
type DirectDeployPageProps = {
|
||||
params: Promise<{
|
||||
@@ -46,18 +46,12 @@ export default async function PublishContractPage(
|
||||
// - get the publish metadata with name+publisher address
|
||||
// - merge the two objects with publishMetadataFromUri taking higher precedence
|
||||
if (!publishMetadataFromUri.version) {
|
||||
const publishedContractVersions =
|
||||
await getPublishedContractsWithPublisherMapping({
|
||||
const publishedContract =
|
||||
await getLatestPublishedContractsWithPublisherMapping({
|
||||
publisher: address,
|
||||
contract_id: publishMetadataFromUri.name,
|
||||
});
|
||||
|
||||
if (!publishedContractVersions) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const publishedContract = publishedContractVersions[0];
|
||||
|
||||
if (publishedContract) {
|
||||
publishMetadata = {
|
||||
...publishedContract,
|
||||
|
||||
@@ -131,3 +131,5 @@ export default async function ExploreCategoryPage(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
@@ -48,3 +48,5 @@ export default async function ExplorePage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
+6
-12
@@ -1,6 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { PublishedContractBreadcrumbs } from "./components/breadcrumbs.client";
|
||||
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
|
||||
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
|
||||
|
||||
type Params = { publisher: string; contract_id: string };
|
||||
|
||||
@@ -20,16 +19,11 @@ export async function generateMetadata(props: { params: Promise<Params> }) {
|
||||
const params = await props.params;
|
||||
const { publisher, contract_id } = params;
|
||||
|
||||
const publishedContracts = await getPublishedContractsWithPublisherMapping({
|
||||
publisher: publisher,
|
||||
contract_id: contract_id,
|
||||
});
|
||||
|
||||
if (!publishedContracts) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const publishedContract = publishedContracts[0];
|
||||
const publishedContract =
|
||||
await getLatestPublishedContractsWithPublisherMapping({
|
||||
publisher: publisher,
|
||||
contract_id: contract_id,
|
||||
});
|
||||
|
||||
if (!publishedContract) {
|
||||
return {
|
||||
|
||||
+3
-9
@@ -3,7 +3,7 @@ import { format } from "date-fns/format";
|
||||
import { resolveEns } from "lib/ens";
|
||||
import { correctAndUniqueLicenses } from "lib/licenses";
|
||||
import { getSocialProfiles } from "thirdweb/social";
|
||||
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
|
||||
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
|
||||
import { publishedContractOGImageTemplate } from "./utils/publishedContractOGImageTemplate";
|
||||
|
||||
export const runtime = "edge";
|
||||
@@ -22,8 +22,8 @@ export default async function Image(props: {
|
||||
const client = getThirdwebClient();
|
||||
const { publisher, contract_id } = props.params;
|
||||
|
||||
const [publishedContracts, socialProfiles] = await Promise.all([
|
||||
getPublishedContractsWithPublisherMapping({
|
||||
const [publishedContract, socialProfiles] = await Promise.all([
|
||||
getLatestPublishedContractsWithPublisherMapping({
|
||||
publisher: publisher,
|
||||
contract_id: contract_id,
|
||||
}),
|
||||
@@ -42,12 +42,6 @@ export default async function Image(props: {
|
||||
};
|
||||
})();
|
||||
|
||||
if (!publishedContracts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publishedContract = publishedContracts[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+25
-1
@@ -1,5 +1,8 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
|
||||
import {
|
||||
fetchLatestPublishedContractVersion,
|
||||
fetchPublishedContractVersions,
|
||||
} from "components/contract-components/fetch-contracts-with-versions";
|
||||
import { isAddress } from "thirdweb";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
|
||||
@@ -36,3 +39,24 @@ export async function getPublishedContractsWithPublisherMapping(options: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLatestPublishedContractsWithPublisherMapping(options: {
|
||||
publisher: string;
|
||||
contract_id: string;
|
||||
}) {
|
||||
const { publisher, contract_id } = options;
|
||||
|
||||
try {
|
||||
// resolve ENS
|
||||
const publisherAddress = isAddress(publisher)
|
||||
? publisher
|
||||
: await resolveAddress({
|
||||
client: getThirdwebClient(),
|
||||
name: mapThirdwebPublisher(publisher),
|
||||
});
|
||||
|
||||
return fetchLatestPublishedContractVersion(publisherAddress, contract_id);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+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" />
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { shareLink } from "@/lib/shareLink";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
export function DeployActions(props: {
|
||||
publisher: string;
|
||||
contract_id: string;
|
||||
version?: string;
|
||||
displayName: string;
|
||||
}) {
|
||||
const searchparams = useSearchParams();
|
||||
|
||||
const stringifiedSearchParams = searchparams?.toString();
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
|
||||
target="_blank"
|
||||
>
|
||||
Contract
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
shareLink({
|
||||
title: `Deploy ${props.displayName}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-1
@@ -19,6 +19,7 @@ export function PublishedActions(props: {
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-card"
|
||||
onClick={() => {
|
||||
shareLink({
|
||||
title: `Deploy ${props.displayName}`,
|
||||
@@ -27,7 +28,7 @@ export function PublishedActions(props: {
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
<Button asChild variant="primary" className="gap-2">
|
||||
<Button asChild className="gap-2">
|
||||
<Link
|
||||
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function DeployContractInfo(props: {
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-1 items-center gap-4">
|
||||
{contractImageLink && (
|
||||
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border p-2 md:flex">
|
||||
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border bg-card p-2 md:flex">
|
||||
{/*eslint-disable-next-line @next/next/no-img-element*/}
|
||||
<img className="size-12" alt={props.name} src={contractImageLink} />
|
||||
</div>
|
||||
|
||||
+3
-11
@@ -1,4 +1,3 @@
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import {
|
||||
fetchPublishedContractVersion,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
import { isAddress } from "thirdweb";
|
||||
import { fetchDeployMetadata } from "thirdweb/contract";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import { DeployActions } from "./contract-actions-deploy.client";
|
||||
import { DeployContractHeader } from "./contract-header";
|
||||
import { DeployFormForUri } from "./uri-based-deploy";
|
||||
|
||||
@@ -73,23 +71,17 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto flex w-full max-w-[1000px] flex-col gap-8 pb-20">
|
||||
<DeployContractHeader
|
||||
{...props}
|
||||
allVersions={publishedContractVersions}
|
||||
activeVersion={publishedContract}
|
||||
>
|
||||
<DeployActions
|
||||
{...props}
|
||||
displayName={publishedContract.displayName || publishedContract.name}
|
||||
/>
|
||||
</DeployContractHeader>
|
||||
<Separator />
|
||||
/>
|
||||
<DeployFormForUri
|
||||
contractMetadata={contractMetadata}
|
||||
modules={fetchedModules.filter((m) => m !== null)}
|
||||
pathname={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy`}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export function DeployContractVersionSelector({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[180px] bg-transparent hover:bg-accent">
|
||||
<SelectTrigger className="min-w-[180px] bg-card hover:bg-accent">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
+40
-36
@@ -1,10 +1,10 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTeamById } from "@/api/team";
|
||||
import { getRawAccount } from "../../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../login/loginRedirect";
|
||||
|
||||
type State = {
|
||||
success: boolean;
|
||||
@@ -71,33 +71,37 @@ export async function createTicketAction(
|
||||
_previousState: State,
|
||||
formData: FormData,
|
||||
) {
|
||||
const cookieManager = await cookies();
|
||||
const activeAccount = cookieManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookieManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
if (!activeAccount || !token) {
|
||||
// user is not logged in, make them log in
|
||||
redirect(`/login?next=${encodeURIComponent("/support")}`);
|
||||
}
|
||||
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (accountRes.status !== 200) {
|
||||
// user is not logged in, make them log in
|
||||
redirect(`/login?next=${encodeURIComponent("/support")}`);
|
||||
const teamId = formData.get("teamId")?.toString();
|
||||
|
||||
if (!teamId) {
|
||||
return {
|
||||
success: false,
|
||||
message: "teamId is required",
|
||||
};
|
||||
}
|
||||
|
||||
const account = (await accountRes.json()) as {
|
||||
data: { name: string; email: string; plan: string; id: string };
|
||||
};
|
||||
const team = await getTeamById(teamId);
|
||||
|
||||
const customerId = isValidPlan(account.data.plan)
|
||||
? planToCustomerId[account.data.plan]
|
||||
: undefined;
|
||||
if (!team) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Team with id "${teamId}" not found`,
|
||||
};
|
||||
}
|
||||
|
||||
const [walletAddress, account] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getRawAccount(),
|
||||
]);
|
||||
|
||||
if (!walletAddress || !account) {
|
||||
loginRedirect("/support");
|
||||
}
|
||||
|
||||
const customerId = isValidPlan(team.supportPlan)
|
||||
? planToCustomerId[team.supportPlan]
|
||||
: // fallback to "free" tier
|
||||
planToCustomerId.free;
|
||||
|
||||
const product = formData.get("product")?.toString() || "";
|
||||
const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
|
||||
@@ -105,8 +109,8 @@ export async function createTicketAction(
|
||||
const title = prepareEmailTitle(
|
||||
product,
|
||||
problemArea,
|
||||
account.data.email,
|
||||
account.data.name,
|
||||
account.email || "",
|
||||
account.name || "",
|
||||
);
|
||||
|
||||
const keyVal: Record<string, string> = {};
|
||||
@@ -117,10 +121,10 @@ export async function createTicketAction(
|
||||
const markdown = prepareEmailBody({
|
||||
product,
|
||||
markdownInput: keyVal.markdown || "",
|
||||
email: account.data.email,
|
||||
name: account.data.name,
|
||||
email: account.email || "",
|
||||
name: account.name || "",
|
||||
extraInfoInput: keyVal,
|
||||
walletAddress: activeAccount,
|
||||
walletAddress: walletAddress,
|
||||
});
|
||||
|
||||
const content = {
|
||||
@@ -129,9 +133,9 @@ export async function createTicketAction(
|
||||
markdown,
|
||||
status: "open",
|
||||
onBehalfOf: {
|
||||
email: account.data.email,
|
||||
name: account.data.name,
|
||||
id: account.data.id,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
id: account.id,
|
||||
},
|
||||
customerId,
|
||||
emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
|
||||
|
||||
+47
-10
@@ -3,6 +3,7 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SupportForm_SelectInput } from "components/help/contact-forms/shared/SupportForm_SelectInput";
|
||||
import dynamic from "next/dynamic";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { SupportForm_TeamSelection } from "../../../../../components/help/contact-forms/shared/SupportForm_TeamSelection";
|
||||
import { createTicketAction } from "./create-ticket.action";
|
||||
|
||||
const ConnectSupportForm = dynamic(
|
||||
@@ -75,14 +77,46 @@ const productOptions: { label: string; component: ReactElement }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function CreateTicket() {
|
||||
function ProductAreaSelection(props: {
|
||||
productLabel: string;
|
||||
setProductLabel: (val: string) => void;
|
||||
}) {
|
||||
const { productLabel, setProductLabel } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SupportForm_SelectInput
|
||||
formLabel="What do you need help with?"
|
||||
name="product"
|
||||
options={productOptions.map((o) => o.label)}
|
||||
promptText="Select a product"
|
||||
onValueChange={setProductLabel}
|
||||
value={productLabel}
|
||||
required={true}
|
||||
/>
|
||||
{productOptions.find((o) => o.label === productLabel)?.component}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateTicket(props: {
|
||||
teams: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
}) {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(
|
||||
props.teams[0]?.id,
|
||||
);
|
||||
|
||||
const [productLabel, setProductLabel] = useState("");
|
||||
|
||||
const [state, formAction] = useActionState(createTicketAction, {
|
||||
message: "",
|
||||
success: false,
|
||||
});
|
||||
|
||||
// needed here
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
@@ -112,16 +146,19 @@ export function CreateTicket() {
|
||||
<div className="h-7" />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<SupportForm_SelectInput
|
||||
formLabel="What do you need help with?"
|
||||
name="product"
|
||||
options={productOptions.map((o) => o.label)}
|
||||
promptText="Select a product"
|
||||
onValueChange={setProductLabel}
|
||||
value={productLabel}
|
||||
required={true}
|
||||
{/* Don't conditionally render this - it has be rendered to submit the input values */}
|
||||
<div className={cn(props.teams.length === 1 && "hidden")}>
|
||||
<SupportForm_TeamSelection
|
||||
selectedTeamId={selectedTeamId}
|
||||
onChange={(teamId) => setSelectedTeamId(teamId)}
|
||||
teams={props.teams}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProductAreaSelection
|
||||
productLabel={productLabel}
|
||||
setProductLabel={setProductLabel}
|
||||
/>
|
||||
{productOptions.find((o) => o.label === productLabel)?.component}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getTeams } from "@/api/team";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -7,17 +8,16 @@ import {
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../login/loginRedirect";
|
||||
import { CreateTicket } from "./components/create-ticket.client";
|
||||
|
||||
export default async function Page() {
|
||||
const authToken = await getAuthToken();
|
||||
const teams = await getTeams();
|
||||
|
||||
if (!authToken) {
|
||||
return redirect(
|
||||
`/login?next=${encodeURIComponent("/support/create-ticket")}`,
|
||||
);
|
||||
const pagePath = "/support/create-ticket";
|
||||
|
||||
if (!teams || teams.length === 0) {
|
||||
loginRedirect(pagePath);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -36,7 +36,12 @@ export default async function Page() {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="container max-w-[750px] py-10">
|
||||
<CreateTicket />
|
||||
<CreateTicket
|
||||
teams={teams.map((t) => ({
|
||||
name: t.name,
|
||||
id: t.id,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -86,6 +86,7 @@ function TailwindTheme(props: { children: React.ReactNode }) {
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
defaultTheme="dark"
|
||||
enableColorScheme={true}
|
||||
>
|
||||
{props.children}
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import analyticsImage from "../../../../public/assets/landingpage/desktop/analyt
|
||||
import authDesktopImage from "../../../../public/assets/landingpage/desktop/auth.png";
|
||||
import crossPlatformDesktopImage from "../../../../public/assets/landingpage/desktop/cross-platform.png";
|
||||
import enterpriseSecurityImage from "../../../../public/assets/landingpage/desktop/enterprise-security.png";
|
||||
import guestImage from "../../../../public/assets/landingpage/desktop/guest.png";
|
||||
import magicImage from "../../../../public/assets/landingpage/desktop/magic.png";
|
||||
import onboardImage from "../../../../public/assets/landingpage/desktop/onboard.png";
|
||||
import powerfulImage from "../../../../public/assets/landingpage/desktop/powerful.png";
|
||||
@@ -24,7 +23,6 @@ import analyticsMobileImage from "../../../../public/assets/landingpage/mobile/a
|
||||
import authMobileImage from "../../../../public/assets/landingpage/mobile/auth.png";
|
||||
import crossPlatformMobileImage from "../../../../public/assets/landingpage/mobile/cross-platform.png";
|
||||
import enterpriseSecurityMobileImage from "../../../../public/assets/landingpage/mobile/enterprise-security.png";
|
||||
import guestMobileImage from "../../../../public/assets/landingpage/mobile/guest.png";
|
||||
import magicMobileImage from "../../../../public/assets/landingpage/mobile/magic.png";
|
||||
import mobileOnboardImage from "../../../../public/assets/landingpage/mobile/onboard.png";
|
||||
import powerfulMobileImage from "../../../../public/assets/landingpage/mobile/powerful.png";
|
||||
@@ -190,16 +188,6 @@ export default function Page() {
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/enable-gasless"
|
||||
/>
|
||||
|
||||
<LandingCardWithImage
|
||||
title="Guest mode"
|
||||
description={`Allow anyone to use your app in seconds — with a wallet that's generated automatically when they press "Continue as guest."`}
|
||||
image={guestImage}
|
||||
mobileImage={guestMobileImage}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/references/wallets/latest/LocalWallet"
|
||||
colSpan={1}
|
||||
/>
|
||||
|
||||
<LandingCardWithImage
|
||||
title="Bring your own auth"
|
||||
description="Integrate your authentication system and spin up in-app wallets for your users — new and existing."
|
||||
|
||||
@@ -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))"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -12,11 +12,11 @@ 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";
|
||||
@@ -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,
|
||||
@@ -363,13 +355,12 @@ export function ChatPageContent(props: {
|
||||
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
|
||||
|
||||
const handleUpdateContextFilters = async (
|
||||
values: ContextFilters | undefined,
|
||||
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 filters
|
||||
// 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,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
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 { useMutation } from "@tanstack/react-query";
|
||||
@@ -31,52 +31,34 @@ 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 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>
|
||||
@@ -84,9 +66,7 @@ export default function ContextFiltersButton(props: {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogTitle className="font-semibold text-xl">Context</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
@@ -105,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(
|
||||
@@ -132,14 +116,13 @@ const formSchema = z.object({
|
||||
message: "Chain IDs must be a comma-separated list of integers",
|
||||
},
|
||||
),
|
||||
contractAddresses: commaSeparateListOfAddresses,
|
||||
walletAddresses: commaSeparateListOfAddresses,
|
||||
walletAddress: optionalAddressSchema,
|
||||
});
|
||||
|
||||
export function ContextFiltersForm(props: {
|
||||
contextFilters: ContextFilters | undefined;
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
setContextFilters: (filters: ContextFilters | undefined) => void;
|
||||
contextFilters: NebulaContext | undefined;
|
||||
updateContextFilters: (filters: NebulaContext | undefined) => Promise<void>;
|
||||
setContextFilters: (filters: NebulaContext | undefined) => void;
|
||||
formBodyClassName?: string;
|
||||
formActionContainerClassName?: string;
|
||||
modal:
|
||||
@@ -158,40 +141,26 @@ export function ContextFiltersForm(props: {
|
||||
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());
|
||||
|
||||
const data = {
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
walletAddresses: walletAddressesArray,
|
||||
walletAddress: walletAddress || null,
|
||||
};
|
||||
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
success: "Context updated",
|
||||
error: "Failed to update context",
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
@@ -228,7 +197,6 @@ export function ContextFiltersForm(props: {
|
||||
.filter(Boolean)
|
||||
.map(Number)}
|
||||
onChange={(values) => {
|
||||
console.log("values", values);
|
||||
form.setValue("chainIds", values.join(","));
|
||||
}}
|
||||
/>
|
||||
@@ -240,33 +208,15 @@ export function ContextFiltersForm(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="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="walletAddresses"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wallet Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
placeholder="0x123.."
|
||||
className="md:text-xs md:placeholder:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -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();
|
||||
|
||||
+4
@@ -38,6 +38,10 @@ const sidebarLinkMeta: Array<{ pathId: string; label: string }> = [
|
||||
pathId: "alerts",
|
||||
label: "Alerts",
|
||||
},
|
||||
{
|
||||
pathId: "wallet-credentials",
|
||||
label: "Wallet Credentials",
|
||||
},
|
||||
{
|
||||
pathId: "configuration",
|
||||
label: "Configuration",
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import {
|
||||
type EngineInstance,
|
||||
type SetWalletConfigInput,
|
||||
useEngineSetWalletConfig,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface CircleConfigProps {
|
||||
instance: EngineInstance;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export const CircleConfig: React.FC<CircleConfigProps> = ({
|
||||
instance,
|
||||
authToken,
|
||||
}) => {
|
||||
const { mutate: setCircleConfig, isPending } = useEngineSetWalletConfig({
|
||||
instanceUrl: instance.url,
|
||||
authToken,
|
||||
});
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const defaultValues: SetWalletConfigInput = {
|
||||
type: "circle" as const,
|
||||
circleApiKey: "",
|
||||
};
|
||||
|
||||
const form = useForm<SetWalletConfigInput>({
|
||||
defaultValues,
|
||||
values: defaultValues,
|
||||
resetOptions: {
|
||||
keepDirty: true,
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: SetWalletConfigInput) => {
|
||||
setCircleConfig(data, {
|
||||
onSuccess: () => {
|
||||
toast.success("Configuration set successfully");
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "set-wallet-config",
|
||||
type: "circle",
|
||||
label: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to set configuration", {
|
||||
description: error.message,
|
||||
});
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "set-wallet-config",
|
||||
type: "circle",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-muted-foreground">
|
||||
Circle wallets require an API Key from your Circle account with
|
||||
sufficient permissions. Created wallets are stored in your AWS
|
||||
account. Configure your Circle API Key to use Circle wallets. Learn
|
||||
more about{" "}
|
||||
<TrackedLinkTW
|
||||
href="https://portal.thirdweb.com/engine/features/backend-wallets#circle-wallet"
|
||||
target="_blank"
|
||||
label="learn-more"
|
||||
category="engine"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
how to get an API Key
|
||||
</TrackedLinkTW>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormFieldSetup
|
||||
label="Circle API Key"
|
||||
errorMessage={
|
||||
form.getFieldState("circleApiKey", form.formState).error?.message
|
||||
}
|
||||
htmlFor="circle-api-key"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Input
|
||||
id="circle-api-key"
|
||||
placeholder="TEST_API_KEY:..."
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
{...form.register("circleApiKey")}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="min-w-28 gap-2"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner className="size-4" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+32
-1
@@ -1,9 +1,11 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineWalletConfig,
|
||||
useHasEngineFeature,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import {
|
||||
EngineBackendWalletOptions,
|
||||
@@ -13,6 +15,7 @@ import { CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Heading } from "tw-components";
|
||||
import { CircleConfig } from "./circle-config";
|
||||
import { KmsAwsConfig } from "./kms-aws-config";
|
||||
import { KmsGcpConfig } from "./kms-gcp-config";
|
||||
import { LocalConfig } from "./local-config";
|
||||
@@ -33,11 +36,28 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
|
||||
authToken,
|
||||
});
|
||||
|
||||
const { isSupported: isWalletCredentialsSupported } = useHasEngineFeature(
|
||||
instance.url,
|
||||
"WALLET_CREDENTIALS",
|
||||
);
|
||||
|
||||
const filteredWalletOptions = EngineBackendWalletOptions.filter(
|
||||
({ key }) =>
|
||||
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
|
||||
(key !== "circle" || isWalletCredentialsSupported) &&
|
||||
// smart wallets don't need separate configuration
|
||||
!key.startsWith("smart:"),
|
||||
);
|
||||
|
||||
const tabContent: Partial<Record<EngineBackendWalletType, React.ReactNode>> =
|
||||
{
|
||||
local: <LocalConfig />,
|
||||
"aws-kms": <KmsAwsConfig instance={instance} authToken={authToken} />,
|
||||
"gcp-kms": <KmsGcpConfig instance={instance} authToken={authToken} />,
|
||||
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
|
||||
...(isWalletCredentialsSupported && {
|
||||
circle: <CircleConfig instance={instance} authToken={authToken} />,
|
||||
}),
|
||||
} as const;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<EngineBackendWalletType>("local");
|
||||
@@ -62,7 +82,7 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
|
||||
</div>
|
||||
|
||||
<TabButtons
|
||||
tabs={EngineBackendWalletOptions.map(({ key, name }) => ({
|
||||
tabs={filteredWalletOptions.map(({ key, name }) => ({
|
||||
key,
|
||||
name,
|
||||
isActive: activeTab === key,
|
||||
@@ -81,6 +101,17 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
|
||||
tabClassName="font-medium !text-sm"
|
||||
/>
|
||||
|
||||
{!isWalletCredentialsSupported && activeTab === "circle" && (
|
||||
<Alert variant="warning" className="mt-4">
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>Update Required</AlertTitle>
|
||||
<AlertDescription>
|
||||
Circle wallet support requires a newer version of Engine. Please
|
||||
update your Engine instance to use this feature.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{tabContent[activeTab]}
|
||||
</div>
|
||||
);
|
||||
|
||||
+49
-20
@@ -6,6 +6,8 @@ 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,
|
||||
@@ -30,7 +32,7 @@ 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";
|
||||
@@ -40,18 +42,21 @@ import { EngineBackendWalletOptions } from "lib/engine";
|
||||
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 { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { FormHelperText, FormLabel, Text } from "tw-components";
|
||||
import { prettyPrintCurrency } from "./utils";
|
||||
|
||||
interface BackendWalletsTableProps {
|
||||
@@ -83,16 +88,17 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
});
|
||||
const { data: backendWalletBalance, isFetching } =
|
||||
useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
});
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
return <Skeleton className="h-5 w-32 rounded-lg" />;
|
||||
if (!backendWalletBalance || isFetching) {
|
||||
return <Skeleton className="h-6 w-36 rounded-lg" />;
|
||||
}
|
||||
|
||||
const balanceDisplay = prettyPrintCurrency({
|
||||
@@ -100,9 +106,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
|
||||
});
|
||||
|
||||
const balanceComponent = (
|
||||
<div className="text-muted-foreground">{balanceDisplay}</div>
|
||||
);
|
||||
const balanceComponent = <span>{balanceDisplay}</span>;
|
||||
|
||||
const explorer = chain.blockExplorers?.[0];
|
||||
if (!explorer) {
|
||||
@@ -110,14 +114,14 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -133,6 +137,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
const receiveDisclosure = useDisclosure();
|
||||
const sendDisclosure = useDisclosure();
|
||||
const deleteDisclosure = useDisclosure();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
@@ -160,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 (
|
||||
@@ -175,7 +204,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
id: "balance",
|
||||
}),
|
||||
];
|
||||
}, [instanceUrl, authToken, chainId]);
|
||||
}, [instanceUrl, authToken, chainId, queryClient]);
|
||||
|
||||
const [selectedBackendWallet, setSelectedBackendWallet] =
|
||||
useState<BackendWallet>();
|
||||
|
||||
+107
-21
@@ -2,6 +2,7 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
@@ -102,6 +103,9 @@ export const CreateBackendWalletButton: React.FC<
|
||||
);
|
||||
invariant(selectedOption, "Selected a valid backend wallet type.");
|
||||
|
||||
const isCircleWallet =
|
||||
walletType === "circle" || walletType === "smart:circle";
|
||||
|
||||
// List all wallet types only if Engine is updated to support it.
|
||||
let walletTypeOptions = [selectedOption];
|
||||
if (supportsSmartBackendWallets) {
|
||||
@@ -126,7 +130,7 @@ export const CreateBackendWalletButton: React.FC<
|
||||
</Button>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
className="z-[10001] p-0"
|
||||
className="z-[10001] overflow-hidden p-0"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<Form {...form}>
|
||||
@@ -138,7 +142,7 @@ export const CreateBackendWalletButton: React.FC<
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Wallet type */}
|
||||
<FormFieldSetup
|
||||
label="Wallet Type"
|
||||
@@ -201,25 +205,107 @@ export const CreateBackendWalletButton: React.FC<
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
// Label
|
||||
<FormFieldSetup
|
||||
key="label"
|
||||
label="Label"
|
||||
errorMessage={
|
||||
form.getFieldState("label", form.formState).error
|
||||
?.message
|
||||
}
|
||||
htmlFor="wallet-label"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Input
|
||||
id="wallet-label"
|
||||
type="text"
|
||||
placeholder="A description to identify this backend wallet"
|
||||
{...form.register("label", { required: true })}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
<>
|
||||
{/* Label */}
|
||||
<FormFieldSetup
|
||||
key="label"
|
||||
label="Label"
|
||||
errorMessage={
|
||||
form.getFieldState("label", form.formState).error
|
||||
?.message
|
||||
}
|
||||
htmlFor="wallet-label"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Input
|
||||
id="wallet-label"
|
||||
type="text"
|
||||
placeholder="A description to identify this backend wallet"
|
||||
{...form.register("label", { required: true })}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
|
||||
{/* Credential ID for Circle wallets */}
|
||||
{isCircleWallet && (
|
||||
<>
|
||||
<FormFieldSetup
|
||||
key="credentialId"
|
||||
label="Credential ID"
|
||||
errorMessage={
|
||||
form.getFieldState("credentialId", form.formState)
|
||||
.error?.message
|
||||
}
|
||||
htmlFor="credential-id"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Input
|
||||
id="credential-id"
|
||||
type="text"
|
||||
placeholder="Enter the Circle credential ID"
|
||||
{...form.register("credentialId", {
|
||||
required: isCircleWallet,
|
||||
})}
|
||||
/>
|
||||
<FormDescription className="py-2">
|
||||
The ID of the Circle credential to use for this
|
||||
wallet. You can find this in the{" "}
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/engine/${instance.id}/wallet-credentials`}
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Wallet Credentials
|
||||
</Link>{" "}
|
||||
section.
|
||||
</FormDescription>
|
||||
</FormFieldSetup>
|
||||
|
||||
<FormFieldSetup
|
||||
key="isTestnet"
|
||||
label="Testnet"
|
||||
errorMessage={
|
||||
form.getFieldState("isTestnet", form.formState)
|
||||
.error?.message
|
||||
}
|
||||
htmlFor="is-testnet"
|
||||
tooltip={null}
|
||||
isRequired={false}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="is-testnet"
|
||||
checked={form.watch("isTestnet")}
|
||||
onCheckedChange={(checked) =>
|
||||
form.setValue("isTestnet", !!checked)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="is-testnet"
|
||||
className="text-sm leading-none opacity-70 peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Use testnet mode for creating backend wallet
|
||||
</label>
|
||||
</div>
|
||||
<FormDescription className="py-2">
|
||||
If your engine is configured with a testnet API
|
||||
Key for Circle, you can only create testnet
|
||||
wallets. A production API Key cannot be used for
|
||||
testnet transactions, and vice versa.{" "}
|
||||
<Link
|
||||
href="https://developers.circle.com/w3s/sandbox-vs-production"
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-3
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineBackendWallets,
|
||||
@@ -80,13 +81,15 @@ function BackendWalletsSection(props: {
|
||||
Configuration
|
||||
</Link>{" "}
|
||||
tab, or{" "}
|
||||
<Link
|
||||
<TrackedLinkTW
|
||||
href="https://portal.thirdweb.com/infrastructure/engine/features/backend-wallets"
|
||||
target="_blank"
|
||||
className="underline decoration-muted-foreground/50 decoration-dotted underline-offset-4 hover:text-foreground hover:decoration-foreground hover:decoration-solid"
|
||||
label="learn-more"
|
||||
category="engine"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
learn more about backend wallets.
|
||||
</Link>
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-3
@@ -52,6 +52,7 @@ import Link from "next/link";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { toTokens } from "thirdweb";
|
||||
import { FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { normalizeTime } from "../../../../../../../../../../lib/time";
|
||||
import { TransactionTimeline } from "./transaction-timeline";
|
||||
|
||||
export type EngineStatus =
|
||||
@@ -496,9 +497,7 @@ export function TransactionCharts(props: {
|
||||
if (!tx.queuedAt || !tx.status) {
|
||||
continue;
|
||||
}
|
||||
const normalizedDate = new Date(tx.queuedAt);
|
||||
normalizedDate.setHours(0, 0, 0, 0); // normalize time
|
||||
const time = normalizedDate.getTime();
|
||||
const time = normalizeTime(new Date(tx.queuedAt)).getTime();
|
||||
const entry = dayToTxCountMap.get(time) ?? {};
|
||||
entry[tx.status] = (entry[tx.status] ?? 0) + 1;
|
||||
uniqueStatuses.add(tx.status);
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEngineCreateWalletCredential } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CredentialForm } from "./credential-form";
|
||||
import type { CredentialFormData } from "./types";
|
||||
|
||||
interface CreateWalletCredentialButtonProps {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export const CreateWalletCredentialButton: React.FC<
|
||||
CreateWalletCredentialButtonProps
|
||||
> = ({ instanceUrl, authToken }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const createCredential = useEngineCreateWalletCredential({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: CredentialFormData) => {
|
||||
const promise = createCredential.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: () => {
|
||||
setIsOpen(false);
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "create-wallet-credential",
|
||||
label: "success",
|
||||
type: data.type,
|
||||
});
|
||||
return "Wallet credential created successfully";
|
||||
},
|
||||
error: (error) => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "create-wallet-credential",
|
||||
label: "error",
|
||||
type: data.type,
|
||||
error,
|
||||
});
|
||||
return "Failed to create wallet credential";
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "open-create-wallet-credential",
|
||||
});
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Wallet Credential
|
||||
</Button>
|
||||
|
||||
<CredentialForm
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
onSubmit={async (data) => handleSubmit(data as CredentialFormData)}
|
||||
title="Create Wallet Credential"
|
||||
submitButtonText="Create"
|
||||
isPending={createCredential.isPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { Dialog } from "@radix-ui/react-dialog";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CircleCredentialFields } from "./credential-type-fields/circle";
|
||||
import {
|
||||
CREDENTIAL_TYPE_OPTIONS,
|
||||
type CredentialFormData,
|
||||
type CredentialType,
|
||||
type CredentialUpdateFormData,
|
||||
} from "./types";
|
||||
|
||||
interface CredentialFormProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (
|
||||
data: CredentialFormData | CredentialUpdateFormData,
|
||||
) => Promise<void>;
|
||||
title: string;
|
||||
submitButtonText: string;
|
||||
isPending: boolean;
|
||||
defaultValues?: Partial<CredentialFormData | CredentialUpdateFormData>;
|
||||
hideTypeSelect?: boolean;
|
||||
isUpdate?: boolean;
|
||||
}
|
||||
|
||||
export const CredentialForm = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
title,
|
||||
submitButtonText,
|
||||
isPending,
|
||||
defaultValues,
|
||||
hideTypeSelect = false,
|
||||
isUpdate = false,
|
||||
}: CredentialFormProps) => {
|
||||
const form = useForm<CredentialFormData | CredentialUpdateFormData>({
|
||||
defaultValues: {
|
||||
type: "circle" as CredentialType,
|
||||
label: "",
|
||||
...defaultValues,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const selectedType = form.watch("type");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="z-[10001] overflow-hidden p-0"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
return onSubmit(data);
|
||||
})}
|
||||
>
|
||||
<div className="p-6">
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<TrackedLinkTW
|
||||
href="https://portal.thirdweb.com/infrastructure/engine/features/wallet-credentials"
|
||||
target="_blank"
|
||||
label="learn-more"
|
||||
category="engine"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more about wallet credentials
|
||||
</TrackedLinkTW>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Type */}
|
||||
{!hideTypeSelect && (
|
||||
<FormFieldSetup
|
||||
label="Type"
|
||||
errorMessage={
|
||||
form.getFieldState("type", form.formState).error?.message
|
||||
}
|
||||
htmlFor="credential-type"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
form.setValue("type", value as CredentialType)
|
||||
}
|
||||
value={form.watch("type")}
|
||||
>
|
||||
<SelectTrigger className="bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10001]">
|
||||
<SelectGroup>
|
||||
{CREDENTIAL_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormFieldSetup>
|
||||
)}
|
||||
|
||||
{/* Label */}
|
||||
<FormFieldSetup
|
||||
label="Label"
|
||||
errorMessage={
|
||||
form.getFieldState("label", form.formState).error?.message
|
||||
}
|
||||
htmlFor="credential-label"
|
||||
isRequired
|
||||
tooltip={null}
|
||||
>
|
||||
<Input
|
||||
id="credential-label"
|
||||
type="text"
|
||||
placeholder="A description to identify this credential"
|
||||
className="bg-card"
|
||||
{...form.register("label", { required: true })}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
|
||||
{/* Type-specific fields */}
|
||||
{selectedType === "circle" && (
|
||||
<CircleCredentialFields form={form} isUpdate={isUpdate} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4 gap-4 border-border border-t bg-card p-6 lg:gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="min-w-28 gap-2"
|
||||
disabled={!form.formState.isValid || isPending}
|
||||
>
|
||||
{isPending && <Spinner className="size-4" />}
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { CredentialFormData, CredentialUpdateFormData } from "../types";
|
||||
|
||||
interface CircleCredentialFieldsProps {
|
||||
form: UseFormReturn<CredentialFormData | CredentialUpdateFormData>;
|
||||
isUpdate?: boolean;
|
||||
}
|
||||
|
||||
export const CircleCredentialFields: React.FC<CircleCredentialFieldsProps> = ({
|
||||
form,
|
||||
isUpdate = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<FormFieldSetup
|
||||
label="Entity Secret"
|
||||
errorMessage={
|
||||
form.getFieldState("entitySecret", form.formState).error?.message
|
||||
}
|
||||
htmlFor="entity-secret"
|
||||
isRequired={!isUpdate}
|
||||
tooltip={null}
|
||||
helperText={
|
||||
<>
|
||||
Entity Secret is a 32-byte private key designed to secure your
|
||||
Developer-Controlled wallets{" "}
|
||||
<Link
|
||||
href="https://developers.circle.com/w3s/entity-secret-management"
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more about entity secret management
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id="entity-secret"
|
||||
type="password"
|
||||
className="bg-card"
|
||||
placeholder={
|
||||
isUpdate
|
||||
? "Leave empty to keep existing secret"
|
||||
: "Your Circle entity secret"
|
||||
}
|
||||
{...form.register("entitySecret", {
|
||||
required: !isUpdate,
|
||||
pattern: {
|
||||
value: /^([0-9a-fA-F]{64})?$/,
|
||||
message:
|
||||
"Entity secret must be a 32-byte hex string (64 characters)",
|
||||
},
|
||||
setValueAs: (value: string) => (value === "" ? undefined : value),
|
||||
})}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
type WalletCredential,
|
||||
useEngineUpdateWalletCredential,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { PencilIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CredentialForm } from "./credential-form";
|
||||
import type { CredentialUpdateFormData } from "./types";
|
||||
|
||||
interface EditWalletCredentialButtonProps {
|
||||
credential: WalletCredential;
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export const EditWalletCredentialButton: React.FC<
|
||||
EditWalletCredentialButtonProps
|
||||
> = ({ credential, instanceUrl, authToken }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const trackEvent = useTrack();
|
||||
const updateCredential = useEngineUpdateWalletCredential({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: CredentialUpdateFormData) => {
|
||||
const promise = updateCredential.mutateAsync({
|
||||
id: credential.id,
|
||||
...data,
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
success: () => {
|
||||
setIsOpen(false);
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "update-wallet-credential",
|
||||
label: "success",
|
||||
type: data.type,
|
||||
});
|
||||
return "Wallet credential updated successfully";
|
||||
},
|
||||
error: (error) => {
|
||||
console.log(error);
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "update-wallet-credential",
|
||||
label: "error",
|
||||
type: data.type,
|
||||
error,
|
||||
});
|
||||
return "Failed to update wallet credential";
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "open-edit-wallet-credential",
|
||||
});
|
||||
setIsOpen(true);
|
||||
}}
|
||||
className="size-8"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<CredentialForm
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
onSubmit={async (data) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
handleSubmit(data as CredentialUpdateFormData).then(resolve);
|
||||
});
|
||||
}}
|
||||
title="Edit Wallet Credential"
|
||||
submitButtonText="Save Changes"
|
||||
isPending={updateCredential.isPending}
|
||||
defaultValues={{
|
||||
type: credential.type as "circle",
|
||||
label: credential.label || "",
|
||||
}}
|
||||
hideTypeSelect
|
||||
isUpdate
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
interface BaseCredentialFormData {
|
||||
type: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type CircleCredentialFormData = BaseCredentialFormData & {
|
||||
type: "circle";
|
||||
entitySecret: string;
|
||||
};
|
||||
|
||||
type CircleCredentialUpdateFormData = BaseCredentialFormData & {
|
||||
type: "circle";
|
||||
entitySecret?: string;
|
||||
};
|
||||
|
||||
export type CredentialFormData = CircleCredentialFormData;
|
||||
export type CredentialUpdateFormData = CircleCredentialUpdateFormData;
|
||||
|
||||
export const CREDENTIAL_TYPE_OPTIONS = [
|
||||
{ value: "circle", label: "Circle" },
|
||||
] as const;
|
||||
|
||||
export type CredentialType = (typeof CREDENTIAL_TYPE_OPTIONS)[number]["value"];
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {} from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { WalletCredential } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { format } from "date-fns";
|
||||
import { EditWalletCredentialButton } from "./edit-wallet-credential-button";
|
||||
|
||||
interface WalletCredentialsTableProps {
|
||||
credentials: WalletCredential[];
|
||||
isPending: boolean;
|
||||
isFetched: boolean;
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export const WalletCredentialsTable: React.FC<WalletCredentialsTableProps> = ({
|
||||
credentials,
|
||||
isPending,
|
||||
isFetched,
|
||||
instanceUrl,
|
||||
authToken,
|
||||
}) => {
|
||||
if (isPending && !isFetched) {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (credentials.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center">
|
||||
<p className="text-muted-foreground text-sm">No credentials found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer className="border-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Updated</TableHead>
|
||||
<TableHead>Default</TableHead>
|
||||
<TableHead className="w-0" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{credentials.map((credential) => (
|
||||
<TableRow key={credential.id}>
|
||||
<TableCell>
|
||||
<CopyTextButton
|
||||
textToCopy={credential.id}
|
||||
textToShow={`${credential.id.slice(0, 8)}...`}
|
||||
variant="secondary"
|
||||
className="font-mono text-xs"
|
||||
tooltip="Copy credential ID"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{credential.label || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{credential.type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(credential.createdAt), "MMM d, yyyy")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(credential.updatedAt), "MMM d, yyyy")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{credential.isDefault ? (
|
||||
<Badge variant="success">Yes</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">No</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<EditWalletCredentialButton
|
||||
credential={credential}
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
useEngineWalletCredentials,
|
||||
useHasEngineFeature,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CreateWalletCredentialButton } from "./create-wallet-credential-button";
|
||||
import { WalletCredentialsTable } from "./wallet-credentials-table";
|
||||
|
||||
interface WalletCredentialsProps {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export const WalletCredentialsSection: React.FC<WalletCredentialsProps> = ({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
}) => {
|
||||
const {
|
||||
data: credentials = [],
|
||||
isPending,
|
||||
isFetched,
|
||||
} = useEngineWalletCredentials({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
|
||||
const {
|
||||
isSupported: supportsWalletCredentials,
|
||||
query: { isPending: isFeatureLoading },
|
||||
} = useHasEngineFeature(instanceUrl, "WALLET_CREDENTIALS");
|
||||
|
||||
if (isFeatureLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!supportsWalletCredentials) {
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>Update Required</AlertTitle>
|
||||
<AlertDescription>
|
||||
Wallet credentials support requires a newer version of Engine. Please
|
||||
update your Engine instance to use this feature.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-border bg-card">
|
||||
<div className="flex flex-col gap-5 border-border border-b p-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Wallet Credentials
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Manage your wallet credentials for different providers.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/infrastructure/engine/features/wallet-credentials"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about wallet credentials.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-4 border-border max-md:w-full max-md:border-t max-md:pt-6">
|
||||
<CreateWalletCredentialButton
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WalletCredentialsTable
|
||||
credentials={credentials}
|
||||
isPending={isPending}
|
||||
isFetched={isFetched}
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { engineInstancePageHandler } from "../../../_utils/getEngineInstancePageMeta";
|
||||
import type { EngineInstancePageProps } from "../types";
|
||||
import { WalletCredentialsSection } from "./components/wallet-credentials";
|
||||
|
||||
export default async function Page(props: EngineInstancePageProps) {
|
||||
const params = await props.params;
|
||||
const { instance, authToken } = await engineInstancePageHandler({
|
||||
engineId: params.engineId,
|
||||
teamSlug: params.team_slug,
|
||||
});
|
||||
|
||||
return (
|
||||
<WalletCredentialsSection
|
||||
instanceUrl={instance.url}
|
||||
authToken={authToken}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,45 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getValidAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-ui";
|
||||
import { NebulaWaitListPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const team = await getTeamBySlug(params.team_slug);
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
props.searchParams,
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
const [account, authToken, team] = await Promise.all([
|
||||
getValidAccount(),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
]);
|
||||
|
||||
if (!team || !authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/~/nebula`);
|
||||
}
|
||||
|
||||
// if nebula access is already granted, redirect to nebula web app
|
||||
const hasNebulaAccess = team.enabledScopes.includes("nebula");
|
||||
|
||||
if (hasNebulaAccess) {
|
||||
redirect("https://nebula.thirdweb.com");
|
||||
return (
|
||||
<NebulaAnalyticsPage
|
||||
accountId={account.id}
|
||||
authToken={authToken}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <NebulaWaitListPage team={team} />;
|
||||
|
||||
+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)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
@@ -45,9 +45,6 @@ export default async function TeamLayout(props: {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const isOnNebulaWaitList = (await getTeamNebulaWaitList(team.slug))
|
||||
?.onWaitlist;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="bg-card">
|
||||
@@ -59,7 +56,6 @@ export default async function TeamLayout(props: {
|
||||
/>
|
||||
<ProjectTabs
|
||||
layoutPath={`/team/${params.team_slug}/${params.project_slug}`}
|
||||
isOnNebulaWaitList={!!isOnNebulaWaitList}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
export type NebulaAnalyticsDataItem = {
|
||||
date: string;
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalSessions: number;
|
||||
totalRequests: number;
|
||||
};
|
||||
|
||||
export const fetchNebulaAnalytics = unstable_cache(
|
||||
async (params: {
|
||||
accountId: string;
|
||||
authToken: string;
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
}) => {
|
||||
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
|
||||
const url = new URL(`${analyticsEndpoint}/v1/nebula/usage`);
|
||||
url.searchParams.set("accountId", params.accountId);
|
||||
url.searchParams.set("from", params.from);
|
||||
url.searchParams.set("to", params.to);
|
||||
url.searchParams.set("interval", params.interval);
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
return {
|
||||
ok: false as const,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
|
||||
const resData = await res.json();
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
data: resData.data as NebulaAnalyticsDataItem[],
|
||||
};
|
||||
},
|
||||
["nebula-analytics"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useResponsiveSearchParams,
|
||||
useSetResponsiveSearchParams,
|
||||
} from "responsive-rsc";
|
||||
import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "../../../../../../../components/analytics/interval-selector";
|
||||
import {
|
||||
getNebulaFiltersFromSearchParams,
|
||||
normalizeTimeISOString,
|
||||
} from "../../../../../../../lib/time";
|
||||
|
||||
export function NebulaAnalyticsFilter() {
|
||||
const responsiveSearchParams = useResponsiveSearchParams();
|
||||
const setResponsiveSearchParams = useSetResponsiveSearchParams();
|
||||
|
||||
const { range, interval } = getNebulaFiltersFromSearchParams({
|
||||
from: responsiveSearchParams.from,
|
||||
to: responsiveSearchParams.to,
|
||||
interval: responsiveSearchParams.interval,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<DateRangeSelector
|
||||
range={range}
|
||||
popoverAlign="end"
|
||||
setRange={(newRange) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
from: normalizeTimeISOString(newRange.from),
|
||||
to: normalizeTimeISOString(newRange.to),
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
interval: newInterval,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { subDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { mobileViewport } from "../../../../../../../stories/utils";
|
||||
import type { NebulaAnalyticsDataItem } from "./fetch-nebula-analytics";
|
||||
import { NebulaAnalyticsDashboardUI } from "./nebula-analytics-ui";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/Analytics",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
type VariantTab = "30-day" | "7-day" | "pending" | "60-day";
|
||||
|
||||
function Story() {
|
||||
const [tab, setTab] = useState<VariantTab>("60-day");
|
||||
return (
|
||||
<div className="container flex flex-col gap-8 py-10">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Story Variants
|
||||
</h2>
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
name: "60 Days",
|
||||
onClick: () => setTab("60-day"),
|
||||
isActive: tab === "60-day",
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "30 Days",
|
||||
onClick: () => setTab("30-day"),
|
||||
isActive: tab === "30-day",
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "7 Days",
|
||||
onClick: () => setTab("7-day"),
|
||||
isActive: tab === "7-day",
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "Pending",
|
||||
onClick: () => setTab("pending"),
|
||||
isActive: tab === "pending",
|
||||
isEnabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tab === "60-day" && (
|
||||
<NebulaAnalyticsDashboardUI
|
||||
data={generateRandomNebulaAnalyticsData(60)}
|
||||
isPending={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "30-day" && (
|
||||
<NebulaAnalyticsDashboardUI
|
||||
data={generateRandomNebulaAnalyticsData(30)}
|
||||
isPending={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "7-day" && (
|
||||
<NebulaAnalyticsDashboardUI
|
||||
data={generateRandomNebulaAnalyticsData(7)}
|
||||
isPending={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "pending" && (
|
||||
<NebulaAnalyticsDashboardUI data={[]} isPending={true} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function generateRandomNebulaAnalyticsData(
|
||||
days: number,
|
||||
): NebulaAnalyticsDataItem[] {
|
||||
return Array.from({ length: days }, (_, i) => ({
|
||||
date: subDays(new Date(), i).toISOString(),
|
||||
totalPromptTokens: randomInt(1000),
|
||||
totalCompletionTokens: randomInt(1000),
|
||||
totalSessions: randomInt(100),
|
||||
totalRequests: randomInt(4000),
|
||||
}));
|
||||
}
|
||||
|
||||
function randomInt(max: number) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import {
|
||||
ActivityIcon,
|
||||
MessageCircleQuestionIcon,
|
||||
MessageSquareIcon,
|
||||
MessageSquareQuoteIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
ResponsiveSearchParamsProvider,
|
||||
ResponsiveSuspense,
|
||||
} from "responsive-rsc";
|
||||
import { normalizeTimeISOString } from "../../../../../../../lib/time";
|
||||
import {
|
||||
type NebulaAnalyticsDataItem,
|
||||
fetchNebulaAnalytics,
|
||||
} from "./fetch-nebula-analytics";
|
||||
import { NebulaAnalyticsFilter } from "./nebula-analytics-filter";
|
||||
import { getNebulaAnalyticsRangeFromSearchParams } from "./utils";
|
||||
|
||||
type ChartData = {
|
||||
time: Date;
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalSessions: number;
|
||||
totalRequests: number;
|
||||
};
|
||||
|
||||
export function NebulaAnalyticsPage(props: {
|
||||
searchParams: {
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
};
|
||||
accountId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={props.searchParams}>
|
||||
<header className="border-b">
|
||||
<div className="container flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
Nebula
|
||||
</h1>
|
||||
</div>
|
||||
<NebulaAnalyticsFilter />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container pt-8 pb-20">
|
||||
<ResponsiveSuspense
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
fallback={<NebulaAnalyticsDashboardUI data={[]} isPending={true} />}
|
||||
>
|
||||
<NebulaAnalyticDashboard
|
||||
searchParams={props.searchParams}
|
||||
accountId={props.accountId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function NebulaAnalyticDashboard(props: {
|
||||
accountId: string;
|
||||
authToken: string;
|
||||
searchParams: {
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
};
|
||||
}) {
|
||||
const { range, interval } = getNebulaAnalyticsRangeFromSearchParams(
|
||||
props.searchParams,
|
||||
);
|
||||
|
||||
const res = await fetchNebulaAnalytics({
|
||||
accountId: props.accountId,
|
||||
authToken: props.authToken,
|
||||
from: normalizeTimeISOString(range.from),
|
||||
to: normalizeTimeISOString(range.to),
|
||||
interval,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center rounded-lg border">
|
||||
<div>
|
||||
<p className="mb-2 text-center text-destructive-text">
|
||||
Failed to fetch Nebula analytics
|
||||
</p>
|
||||
<p className="text-muted-foreground">{res.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <NebulaAnalyticsDashboardUI data={res.data} isPending={false} />;
|
||||
}
|
||||
|
||||
export function NebulaAnalyticsDashboardUI(props: {
|
||||
data: NebulaAnalyticsDataItem[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const data = useMemo(() => {
|
||||
const val: {
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalSessions: number;
|
||||
totalRequests: number;
|
||||
chartData: ChartData[];
|
||||
} = {
|
||||
totalPromptTokens: 0,
|
||||
totalCompletionTokens: 0,
|
||||
totalSessions: 0,
|
||||
totalRequests: 0,
|
||||
chartData: [],
|
||||
};
|
||||
|
||||
for (const item of props.data) {
|
||||
val.totalPromptTokens += item.totalPromptTokens;
|
||||
val.totalCompletionTokens += item.totalCompletionTokens;
|
||||
val.totalSessions += item.totalSessions;
|
||||
val.totalRequests += item.totalRequests;
|
||||
val.chartData.push({
|
||||
totalPromptTokens: item.totalPromptTokens,
|
||||
totalCompletionTokens: item.totalCompletionTokens,
|
||||
totalSessions: item.totalSessions,
|
||||
totalRequests: item.totalRequests,
|
||||
time: new Date(item.date),
|
||||
});
|
||||
}
|
||||
|
||||
return val;
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatCard
|
||||
title="Requests sent"
|
||||
value={data.totalRequests}
|
||||
icon={ActivityIcon}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
<StatCard
|
||||
title="Sessions created"
|
||||
value={data.totalSessions}
|
||||
icon={MessageSquareIcon}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
<StatCard
|
||||
title="Prompt tokens received"
|
||||
value={data.totalPromptTokens}
|
||||
icon={MessageCircleQuestionIcon}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
<StatCard
|
||||
title="Response tokens sent"
|
||||
value={data.totalCompletionTokens}
|
||||
icon={MessageSquareQuoteIcon}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<ThirdwebBarChart
|
||||
data={data.chartData}
|
||||
isPending={props.isPending}
|
||||
title="Requests sent"
|
||||
description="Total number of requests sent to the Nebula from users"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
config={{
|
||||
totalRequests: {
|
||||
label: "Total requests",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ThirdwebBarChart
|
||||
data={data.chartData}
|
||||
isPending={props.isPending}
|
||||
title="Sessions created"
|
||||
description="Total chat sessions created by users. A session is a conversation thread between a user and the Nebula"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
config={{
|
||||
totalSessions: {
|
||||
label: "Total sessions",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ThirdwebBarChart
|
||||
data={data.chartData}
|
||||
isPending={props.isPending}
|
||||
title="Prompt tokens received"
|
||||
description="Total tokens sent from users to Nebula as prompts"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
config={{
|
||||
totalPromptTokens: {
|
||||
label: "Total prompt tokens",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ThirdwebBarChart
|
||||
data={data.chartData}
|
||||
isPending={props.isPending}
|
||||
title="Response tokens sent"
|
||||
description="Total tokens sent from Nebula to users as responses"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
config={{
|
||||
totalCompletionTokens: {
|
||||
label: "Total response tokens",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard(props: {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
isPending: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h2 className="text-sm lg:text-base">{props.title}</h2>
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<SkeletonContainer
|
||||
className="inline-block"
|
||||
skeletonData={10000}
|
||||
loadedData={props.isPending ? undefined : props.value}
|
||||
render={(v) => (
|
||||
<p className="font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
{v}
|
||||
</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { getNebulaFiltersFromSearchParams } from "../../../../../../../lib/time";
|
||||
|
||||
export function getNebulaAnalyticsRangeFromSearchParams(searchParams: {
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
}) {
|
||||
return getNebulaFiltersFromSearchParams({
|
||||
from: searchParams.from,
|
||||
to: searchParams.to,
|
||||
interval: searchParams.interval,
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user