diff --git a/apps/dashboard/next-env.d.ts b/apps/dashboard/next-env.d.ts index 3cd7048ed94..1b3be0840f3 100644 --- a/apps/dashboard/next-env.d.ts +++ b/apps/dashboard/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 327f18abc0a..2a7a509e9d4 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -70,7 +70,6 @@ "lucide-react": "0.468.0", "next": "15.1.3", "next-plausible": "^3.12.4", - "next-seo": "^6.5.0", "next-themes": "^0.4.4", "nextjs-toploader": "^1.6.12", "openapi-types": "^12.1.3", diff --git a/apps/dashboard/src/@/actions/emailSignup.ts b/apps/dashboard/src/@/actions/emailSignup.ts new file mode 100644 index 00000000000..fdd03cf3a7b --- /dev/null +++ b/apps/dashboard/src/@/actions/emailSignup.ts @@ -0,0 +1,28 @@ +"use server"; + +type EmailSignupParams = { + email: string; + send_welcome_email?: boolean; +}; + +export async function emailSignup(payLoad: EmailSignupParams) { + const response = await fetch( + "https://api.beehiiv.com/v2/publications/pub_9f54090a-6d14-406b-adfd-dbb30574f664/subscriptions", + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.BEEHIIV_API_KEY}`, + }, + method: "POST", + body: JSON.stringify({ + email: payLoad.email, + send_welcome_email: payLoad.send_welcome_email || false, + utm_source: "thirdweb.com", + }), + }, + ); + + return { + status: response.status, + }; +} diff --git a/apps/dashboard/src/pages/api/moralis/balances.ts b/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts similarity index 74% rename from apps/dashboard/src/pages/api/moralis/balances.ts rename to apps/dashboard/src/@/actions/getBalancesFromMoralis.ts index 9ceb54115a8..9c82d9e850a 100644 --- a/apps/dashboard/src/pages/api/moralis/balances.ts +++ b/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts @@ -1,15 +1,11 @@ +"use server"; + import { getThirdwebClient } from "@/constants/thirdweb.server"; import { defineDashboardChain } from "lib/defineDashboardChain"; -import type { NextApiRequest, NextApiResponse } from "next"; import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb"; import { getWalletBalance } from "thirdweb/wallets"; -export type BalanceQueryRequest = { - chainId: number; - address: string; -}; - -export type BalanceQueryResponse = Array<{ +type BalanceQueryResponse = Array<{ balance: string; decimals: number; name?: string; @@ -18,21 +14,30 @@ export type BalanceQueryResponse = Array<{ display_balance: string; }>; -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method !== "POST") { - return res.status(400).json({ error: "invalid method" }); - } +export async function getTokenBalancesFromMoralis(params: { + contractAddress: string; + chainId: number; +}): Promise< + | { data: BalanceQueryResponse; error: undefined } + | { + data: undefined; + error: string; + } +> { + const { contractAddress, chainId } = params; - const { chainId, address } = req.body; - if (!isAddress(address)) { - return res.status(400).json({ error: "invalid address" }); + if (!isAddress(contractAddress)) { + return { + data: undefined, + error: "invalid address", + }; } const getNativeBalance = async (): Promise => { // eslint-disable-next-line no-restricted-syntax const chain = defineDashboardChain(chainId, undefined); const balance = await getWalletBalance({ - address, + address: contractAddress, chain, client: getThirdwebClient(), }); @@ -50,7 +55,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const getTokenBalances = async (): Promise => { const _chain = encodeURIComponent(`0x${chainId?.toString(16)}`); - const _address = encodeURIComponent(address); + const _address = encodeURIComponent(contractAddress); const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`; const resp = await fetch(tokenBalanceEndpoint, { @@ -59,6 +64,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { "x-api-key": process.env.MORALIS_API_KEY || "", }, }); + if (!resp.ok) { resp.body?.cancel(); return []; @@ -76,7 +82,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { getTokenBalances(), ]); - return res.status(200).json([...nativeBalance, ...tokenBalances]); -}; - -export default handler; + return { + error: undefined, + data: [...nativeBalance, ...tokenBalances], + }; +} diff --git a/apps/dashboard/src/@/actions/getWalletNFTs.ts b/apps/dashboard/src/@/actions/getWalletNFTs.ts new file mode 100644 index 00000000000..519237727a8 --- /dev/null +++ b/apps/dashboard/src/@/actions/getWalletNFTs.ts @@ -0,0 +1,117 @@ +"use server"; + +import { + generateAlchemyUrl, + isAlchemySupported, + transformAlchemyResponseToNFT, +} from "lib/wallet/nfts/alchemy"; +import { + generateMoralisUrl, + isMoralisSupported, + transformMoralisResponseToNFT, +} from "lib/wallet/nfts/moralis"; +import { + generateSimpleHashUrl, + isSimpleHashSupported, + transformSimpleHashResponseToNFT, +} from "lib/wallet/nfts/simpleHash"; +import type { WalletNFT } from "lib/wallet/nfts/types"; + +type WalletNFTApiReturn = + | { result: WalletNFT[]; error?: undefined } + | { result?: undefined; error: string }; + +export async function getWalletNFTs(params: { + chainId: number; + owner: string; +}): Promise { + const { chainId, owner } = params; + const supportedChainSlug = await isSimpleHashSupported(chainId); + + if (supportedChainSlug && process.env.SIMPLEHASH_API_KEY) { + const url = generateSimpleHashUrl({ chainSlug: supportedChainSlug, owner }); + + const response = await fetch(url, { + method: "GET", + headers: { + "X-API-KEY": process.env.SIMPLEHASH_API_KEY, + }, + next: { + revalidate: 10, // cache for 10 seconds + }, + }); + + if (response.status >= 400) { + return { + error: response.statusText, + }; + } + + try { + const parsedResponse = await response.json(); + const result = await transformSimpleHashResponseToNFT( + parsedResponse, + owner, + ); + + return { result }; + } catch { + return { error: "error parsing response" }; + } + } + + if (isAlchemySupported(chainId)) { + const url = generateAlchemyUrl({ chainId, owner }); + + const response = await fetch(url, { + next: { + revalidate: 10, // cache for 10 seconds + }, + }); + if (response.status >= 400) { + return { error: response.statusText }; + } + try { + const parsedResponse = await response.json(); + const result = await transformAlchemyResponseToNFT(parsedResponse, owner); + + return { result, error: undefined }; + } catch (err) { + console.error("Error fetching NFTs", err); + return { error: "error parsing response" }; + } + } + + if (isMoralisSupported(chainId) && process.env.MORALIS_API_KEY) { + const url = generateMoralisUrl({ chainId, owner }); + + const response = await fetch(url, { + method: "GET", + headers: { + "X-API-Key": process.env.MORALIS_API_KEY, + }, + next: { + revalidate: 10, // cache for 10 seconds + }, + }); + + if (response.status >= 400) { + return { error: response.statusText }; + } + + try { + const parsedResponse = await response.json(); + const result = await transformMoralisResponseToNFT( + await parsedResponse, + owner, + ); + + return { result }; + } catch (err) { + console.error("Error fetching NFTs", err); + return { error: "error parsing response" }; + } + } + + return { error: "unsupported chain" }; +} diff --git a/apps/dashboard/src/@/actions/proxies.ts b/apps/dashboard/src/@/actions/proxies.ts new file mode 100644 index 00000000000..0d5551689d3 --- /dev/null +++ b/apps/dashboard/src/@/actions/proxies.ts @@ -0,0 +1,98 @@ +"use server"; + +import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { API_SERVER_URL } from "../constants/env"; + +type ProxyActionParams = { + pathname: string; + searchParams?: Record; + method: "GET" | "POST" | "PUT" | "DELETE"; + body?: string; + headers?: Record; +}; + +type ProxyActionResult = + | { + status: number; + ok: true; + data: T; + } + | { + status: number; + ok: false; + error: string; + }; + +async function proxy( + baseUrl: string, + params: ProxyActionParams, +): Promise> { + const authToken = await getAuthToken(); + + // build URL + const url = new URL(baseUrl); + url.pathname = params.pathname; + if (params.searchParams) { + for (const key in params.searchParams) { + url.searchParams.append(key, params.searchParams[key] as string); + } + } + + const res = await fetch(url, { + method: params.method, + headers: { + ...params.headers, + ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), + }, + body: params.body, + }); + + if (!res.ok) { + try { + const errorMessage = await res.text(); + return { + status: res.status, + ok: false, + error: errorMessage || res.statusText, + }; + } catch { + return { + status: res.status, + ok: false, + error: res.statusText, + }; + } + } + + return { + status: res.status, + ok: true, + data: await res.json(), + }; +} + +export async function analyticsServerProxy( + params: ProxyActionParams, +) { + return proxy( + process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com", + params, + ); +} + +export async function apiServerProxy( + params: ProxyActionParams, +) { + return proxy(API_SERVER_URL, params); +} + +export async function payServerProxy( + params: ProxyActionParams, +) { + return proxy( + process.env.NEXT_PUBLIC_PAY_URL + ? `https://${process.env.NEXT_PUBLIC_PAY_URL}` + : "https://pay.thirdweb-dev.com", + params, + ); +} diff --git a/apps/dashboard/src/@/components/blocks/wallet-address.tsx b/apps/dashboard/src/@/components/blocks/wallet-address.tsx index a4fc5fd4b6c..258b7527adb 100644 --- a/apps/dashboard/src/@/components/blocks/wallet-address.tsx +++ b/apps/dashboard/src/@/components/blocks/wallet-address.tsx @@ -121,7 +121,11 @@ export function WalletAddress(props: { > {walletAvatarLink && ( - + {profile.name && ( {profile.name.slice(0, 2)} diff --git a/apps/dashboard/src/@/components/ui/tooltip.tsx b/apps/dashboard/src/@/components/ui/tooltip.tsx index 89ce65136e7..392c1740796 100644 --- a/apps/dashboard/src/@/components/ui/tooltip.tsx +++ b/apps/dashboard/src/@/components/ui/tooltip.tsx @@ -36,6 +36,8 @@ export function ToolTipLabel(props: { leftIcon?: React.ReactNode; hoverable?: boolean; contentClassName?: string; + side?: "top" | "right" | "bottom" | "left"; + align?: "center" | "start" | "end"; }) { if (!props.label) { return props.children; @@ -48,6 +50,8 @@ export function ToolTipLabel(props: { {props.children} { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/credits`, { + type Result = { + data: BillingCredit[]; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/account/credits", method: "GET", headers: { "Content-Type": "application/json", }, }); - const json = await res.json(); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); } - const credits = (json.data as BillingCredit[]).filter( + const credits = json.data.filter( (credit) => credit.remainingValueUsdCents > 0 && (!credit.expiresAt || credit.expiresAt > new Date().toISOString()) && @@ -241,6 +252,8 @@ export function useAccountCredits() { }); } +type UserOpUsageQueryResult = (UserOpStats & { chainId?: string })[]; + async function getUserOpUsage(args: { clientId: string; from?: Date; @@ -249,32 +262,35 @@ async function getUserOpUsage(args: { }) { const { clientId, from, to, period } = args; - const searchParams = new URLSearchParams(); - searchParams.append("clientId", clientId); + const searchParams: Record = { + clientId, + }; + if (from) { - searchParams.append("from", from.toISOString()); + searchParams.from = from.toISOString(); } if (to) { - searchParams.append("to", to.toISOString()); + searchParams.to = to.toISOString(); } if (period) { - searchParams.append("period", period); + searchParams.period = period; } - const res = await fetch( - `${THIRDWEB_ANALYTICS_API_HOST}/v1/user-ops?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + + const res = await analyticsServerProxy<{ data: UserOpUsageQueryResult }>({ + pathname: "/v1/user-ops", + method: "GET", + searchParams: searchParams, + headers: { + "Content-Type": "application/json", }, - ); - const json = await res.json(); + }); - if (res.status !== 200) { - throw new Error(json.message); + if (!res.ok) { + throw new Error(res.error); } + const json = res.data; + return json.data; } @@ -297,13 +313,12 @@ export function useUserOpUsageAggregate(args: { "all", ), queryFn: async () => { - const userOpStats: (UserOpStats & { chainId?: string })[] = - await getUserOpUsage({ - clientId, - from, - to, - period: "all", - }); + const userOpStats = await getUserOpUsage({ + clientId, + from, + to, + period: "all", + }); // Aggregate stats across wallet types return userOpStats.reduce( @@ -369,7 +384,13 @@ export function useUpdateAccount() { return useMutation({ mutationFn: async (input: UpdateAccountInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/account`, { + type Result = { + data: object; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/account", method: "PUT", headers: { "Content-Type": "application/json", @@ -377,7 +398,11 @@ export function useUpdateAccount() { body: JSON.stringify(input), }); - const json = await res.json(); + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -400,15 +425,25 @@ export function useUpdateNotifications() { return useMutation({ mutationFn: async (input: UpdateAccountNotificationsInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, { - method: "PUT", + type Result = { + data: object; + error?: { message: string }; + }; + const res = await apiServerProxy({ + pathname: "/v1/account/notifications", + method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ preferences: input }), }); - const json = await res.json(); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -430,21 +465,31 @@ export function useConfirmEmail() { return useMutation({ mutationFn: async (input: ConfirmEmailInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, { - method: "PUT", + type Result = { + error?: { message: string }; + data: { team: Team; account: Account }; + }; + const res = await apiServerProxy({ + pathname: "/v1/account/confirmEmail", + method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(input), }); - const json = await res.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 as { team: Team; account: Account }; + return json.data; }, onSuccess: async () => { // invalidate related cache, since could be relinking account @@ -469,18 +514,25 @@ export function useResendEmailConfirmation() { return useMutation({ mutationFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/account/resendEmailConfirmation`, - { - method: "POST", + type Result = { + error?: { message: string }; + data: object; + }; - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), + const res = await apiServerProxy({ + pathname: "/v1/account/resendEmailConfirmation", + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); - const json = await res.json(); + body: JSON.stringify({}), + }); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -503,18 +555,29 @@ export function useApiKeys(props: { return useQuery({ queryKey: apiKeys.keys(address || ""), queryFn: async () => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, { + type Result = { + data: ApiKey[]; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/keys", method: "GET", headers: { "Content-Type": "application/json", }, }); - const json = await res.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 as ApiKey[]; + return json.data; }, enabled: !!address && props.isLoggedIn, }); @@ -526,22 +589,32 @@ export function useCreateApiKey() { return useMutation({ mutationFn: async (input: CreateKeyInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, { + type Result = { + data: ApiKey; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/keys", method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(input), }); - const json = await res.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 as ApiKey; + return json.data; }, - onSuccess: () => { return queryClient.invalidateQueries({ queryKey: apiKeys.keys(address || ""), @@ -556,16 +629,25 @@ export function useUpdateApiKey() { return useMutation({ mutationFn: async (input: UpdateKeyInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, { - method: "PUT", + type Result = { + data: ApiKey; + error?: { message: string }; + }; + const res = await apiServerProxy({ + pathname: `/v1/keys/${input.id}`, + method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(input), }); - const json = await res.json(); + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -587,14 +669,25 @@ export function useRevokeApiKey() { return useMutation({ mutationFn: async (id: string) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, { + type Result = { + data: ApiKey; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: `/v1/keys/${id}/revoke`, method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({}), }); - const json = await res.json(); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -614,21 +707,35 @@ export const usePolicies = (serviceId?: string) => { return useQuery({ queryKey: ["policies", serviceId], queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/policies?serviceId=${serviceId}`, - { - method: "GET", + if (!serviceId) { + throw new Error(); + } - headers: { - "Content-Type": "application/json", - }, + type Result = { + data: ApiKeyServicePolicy; + error?: { message: string }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/policies", + method: "GET", + headers: { + "Content-Type": "application/json", }, - ); - const json = await res.json(); + searchParams: { + serviceId, + }, + }); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); } - return json.data as ApiKeyServicePolicy; + return json.data; }, enabled: !!serviceId, }); @@ -641,9 +748,14 @@ export const useUpdatePolicies = () => { serviceId: string; data: ApiKeyServicePolicy; }) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, { - method: "POST", + type Result = { + data: ApiKeyServicePolicy; + error?: { message: string }; + }; + const res = await apiServerProxy({ + pathname: "/v1/policies", + method: "POST", headers: { "Content-Type": "application/json", }, @@ -652,11 +764,16 @@ export const useUpdatePolicies = () => { data: input.data, }), }); - const json = await res.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 as ApiKeyServicePolicy; + return json.data; }, onSuccess: (_, variables) => { return queryClient.invalidateQueries({ @@ -673,19 +790,25 @@ export function useRevokeAuthorizedWallet() { return useMutation({ mutationFn: async (variables: { authorizedWalletId: string }) => { const { authorizedWalletId } = variables; + type Result = { + data: object; + error?: { message: string }; + }; - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/authorized-wallets/${authorizedWalletId}/revoke`, - { - method: "POST", - - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), + const res = await apiServerProxy({ + pathname: `/v1/authorized-wallets/${authorizedWalletId}/revoke`, + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); - const json = await res.json(); + body: JSON.stringify({}), + }); + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data; if (json.error) { throw new Error(json.error.message); @@ -706,20 +829,30 @@ export function useAuthorizedWallets() { return useQuery({ queryKey: authorizedWallets.authorizedWallets(address || ""), queryFn: async () => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/authorized-wallets`, { - method: "GET", + type Result = { + data: AuthorizedWallet[]; + error?: { message: string }; + }; + const res = await apiServerProxy({ + pathname: "/v1/authorized-wallets", + method: "GET", headers: { "Content-Type": "application/json", }, }); - const json = await res.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 as AuthorizedWallet[]; + return json.data; }, enabled: !!address, gcTime: 0, diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useContractRoles.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useContractRoles.ts index c9aa7444d5e..80a93fbeda4 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useContractRoles.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useContractRoles.ts @@ -1,7 +1,6 @@ import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; import { hasRole } from "thirdweb/extensions/permissions"; import { useActiveAccount, useReadContract } from "thirdweb/react"; -import type { RequiredParam } from "utils/types"; export function useIsAdmin(contract: ThirdwebContract, failOpen = true) { const address = useActiveAccount()?.address; @@ -19,7 +18,7 @@ export function useIsAdmin(contract: ThirdwebContract, failOpen = true) { export function useIsAdminOrSelf( contract: ThirdwebContract, - self: RequiredParam, + self: string | null | undefined, ) { const address = useActiveAccount()?.address; const isAdmin = useIsAdmin(contract); diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts index 4bbc9ba004d..ede211565a2 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts @@ -1,8 +1,8 @@ "use client"; +import { apiServerProxy } from "@/actions/proxies"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes"; -import { THIRDWEB_API_HOST } from "constants/urls"; import type { EngineBackendWalletType } from "lib/engine"; import { useState } from "react"; import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; @@ -53,15 +53,23 @@ export function useEngineInstances() { return useQuery({ queryKey: engineKeys.instances(address || ""), queryFn: async (): Promise => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, { + type Result = { + data?: { + instances: EngineInstance[]; + }; + }; + + const res = await apiServerProxy({ + pathname: "/v1/engine", method: "GET", }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - const instances = (json.data?.instances as EngineInstance[]) || []; + const json = res.data; + const instances = json.data?.instances || []; return instances.map((instance) => { // Sanitize: Add trailing slash if not present. @@ -210,16 +218,19 @@ export function useEngineGetDeploymentPublicConfiguration( return useQuery({ queryKey: engineKeys.deploymentPublicConfiguration(), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/public-configuration`, - { method: "GET" }, - ); + const res = await apiServerProxy<{ + data: DeploymentPublicConfigurationResponse; + }>({ + pathname: `/v1/teams/${input.teamSlug}/engine/deployments/public-configuration`, + method: "GET", + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - return json.data as DeploymentPublicConfigurationResponse; + const json = res.data; + return json.data; }, }); } @@ -233,22 +244,19 @@ interface UpdateDeploymentInput { export function useEngineUpdateDeployment() { return useMutation({ mutationFn: async (input: UpdateDeploymentInput) => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/${input.deploymentId}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - serverVersion: input.serverVersion, - }), + const res = await apiServerProxy({ + pathname: `/v1/teams/${input.teamSlug}/engine/deployments/${input.deploymentId}`, + method: "PUT", + headers: { + "Content-Type": "application/json", }, - ); - // we never use the response body - res.body?.cancel(); + body: JSON.stringify({ + serverVersion: input.serverVersion, + }), + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } }, }); @@ -262,13 +270,13 @@ export function useEngineRemoveFromDashboard() { mutationFn: async (instanceId: string) => { invariant(instanceId, "instance is required"); - const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, { + const res = await apiServerProxy({ + pathname: `/v1/engine/${instanceId}`, method: "DELETE", }); - // we never use the response body - res.body?.cancel(); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } }, @@ -296,20 +304,17 @@ export function useEngineDeleteCloudHosted() { reason, feedback, }: DeleteCloudHostedInput) => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v2/engine/deployments/${deploymentId}/infrastructure/delete`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ reason, feedback }), + const res = await apiServerProxy({ + pathname: `/v2/engine/deployments/${deploymentId}/infrastructure/delete`, + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); - // we never use the response body - res.body?.cancel(); + body: JSON.stringify({ reason, feedback }), + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } }, @@ -333,21 +338,19 @@ export function useEngineEditInstance() { return useMutation({ mutationFn: async ({ instanceId, name, url }: EditEngineInstanceInput) => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, { + const res = await apiServerProxy({ + pathname: `/v1/engine/${instanceId}`, method: "PUT", - headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name, url }), }); - // we never use the response body - res.body?.cancel(); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } }, - onSuccess: () => { return queryClient.invalidateQueries({ queryKey: engineKeys.instances(address || ""), @@ -1675,14 +1678,16 @@ export function useEngineSystemMetrics(engineId: string) { return useQuery({ queryKey: engineKeys.systemMetrics(engineId), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`, - ); + const res = await apiServerProxy({ + method: "GET", + pathname: `/v1/engine/${engineId}/metrics`, + }); + if (!res.ok) { setEnabled(false); - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = (await res.json()) as EngineResourceMetrics; + const json = res.data as EngineResourceMetrics; return json; }, @@ -1708,16 +1713,19 @@ export function useEngineAlertRules(engineId: string) { return useQuery({ queryKey: engineKeys.alertRules(engineId), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/alert-rules`, - { method: "GET" }, - ); + const res = await apiServerProxy<{ + data: EngineAlertRule[]; + }>({ + pathname: `/v1/engine/${engineId}/alert-rules`, + method: "GET", + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - return json.data as EngineAlertRule[]; + const json = res.data; + return json.data; }, }); } @@ -1734,16 +1742,23 @@ export function useEngineAlerts(engineId: string, limit: number, offset = 0) { return useQuery({ queryKey: engineKeys.alerts(engineId), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/alerts?limit=${limit}&offset=${offset}`, - { method: "GET" }, - ); + const res = await apiServerProxy<{ + data: EngineAlert[]; + }>({ + pathname: `/v1/engine/${engineId}/alerts`, + searchParams: { + limit: `${limit}`, + offset: `${offset}`, + }, + method: "GET", + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - return json.data as EngineAlert[]; + const json = res.data; + return json.data; }, }); } @@ -1778,16 +1793,19 @@ export function useEngineNotificationChannels(engineId: string) { return useQuery({ queryKey: engineKeys.notificationChannels(engineId), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`, - { method: "GET" }, - ); + const res = await apiServerProxy<{ + data: EngineNotificationChannel[]; + }>({ + pathname: `/v1/engine/${engineId}/notification-channels`, + method: "GET", + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - return json.data as EngineNotificationChannel[]; + const json = res.data; + return json.data; }, }); } @@ -1803,22 +1821,23 @@ export function useEngineCreateNotificationChannel(engineId: string) { return useMutation({ mutationFn: async (input: CreateNotificationChannelInput) => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(input), + const res = await apiServerProxy<{ + data: EngineNotificationChannel; + }>({ + pathname: `/v1/engine/${engineId}/notification-channels`, + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify(input), + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - const json = await res.json(); - return json.data as EngineNotificationChannel; + const json = res.data; + return json.data; }, onSuccess: () => { return queryClient.invalidateQueries({ @@ -1833,16 +1852,14 @@ export function useEngineDeleteNotificationChannel(engineId: string) { return useMutation({ mutationFn: async (notificationChannelId: string) => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`, - { - method: "DELETE", - }, - ); + const res = await apiServerProxy({ + pathname: `/v1/engine/${engineId}/notification-channels/${notificationChannelId}`, + method: "DELETE", + }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); + throw new Error(res.error); } - res.body?.cancel(); }, onSuccess: () => { return queryClient.invalidateQueries({ diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts index c1d71c20463..46f8c502b0c 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts @@ -1,47 +1,36 @@ +import { getTokenBalancesFromMoralis } from "@/actions/getBalancesFromMoralis"; import { queryOptions, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query"; -import type { - BalanceQueryRequest, - BalanceQueryResponse, -} from "pages/api/moralis/balances"; import { toast } from "sonner"; import { type ThirdwebContract, sendAndConfirmTransaction } from "thirdweb"; import { distribute, distributeByToken } from "thirdweb/extensions/split"; import { useActiveAccount } from "thirdweb/react"; import invariant from "tiny-invariant"; -async function getSplitBalances(contract: ThirdwebContract) { - const query = await fetch("/api/moralis/balances", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - chainId: contract.chain.id, - address: contract.address, - } as BalanceQueryRequest), - }); - - if (query.status >= 400) { - throw new Error(await query.json().then((r) => r.error)); - } - return query.json() as Promise; -} - -function getQuery(contract: ThirdwebContract) { +function getTokenBalancesQuery(contract: ThirdwebContract) { return queryOptions({ queryKey: ["split-balances", contract.chain.id, contract.address], - queryFn: () => getSplitBalances(contract), + queryFn: async () => { + const res = await getTokenBalancesFromMoralis({ + chainId: contract.chain.id, + contractAddress: contract.address, + }); + + if (!res.data) { + throw new Error(res.error); + } + return res.data; + }, retry: false, }); } export function useSplitBalances(contract: ThirdwebContract) { - return useQuery(getQuery(contract)); + return useQuery(getTokenBalancesQuery(contract)); } export function useSplitDistributeFunds(contract: ThirdwebContract) { @@ -53,8 +42,8 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) { invariant(account, "No active account"); const balances = // get the cached data if it exists, otherwise fetch it - queryClient.getQueryData(getQuery(contract).queryKey) || - (await queryClient.fetchQuery(getQuery(contract))); + queryClient.getQueryData(getTokenBalancesQuery(contract).queryKey) || + (await queryClient.fetchQuery(getTokenBalancesQuery(contract))); const distributions = balances .filter((token) => token.display_balance !== "0.0") @@ -80,7 +69,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) { return await Promise.all(distributions); }, onSettled: () => { - queryClient.invalidateQueries(getQuery(contract)); + queryClient.invalidateQueries(getTokenBalancesQuery(contract)); }, }); } diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useWalletNFTs.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useWalletNFTs.ts index e281d2bc9c6..cdb8f9e3125 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useWalletNFTs.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useWalletNFTs.ts @@ -1,5 +1,6 @@ +import { getWalletNFTs } from "@/actions/getWalletNFTs"; import { useQuery } from "@tanstack/react-query"; -import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]"; +import invariant from "tiny-invariant"; export function useWalletNFTs(params: { chainId: number; @@ -8,10 +9,11 @@ export function useWalletNFTs(params: { return useQuery({ queryKey: ["walletNfts", params.chainId, params.walletAddress], queryFn: async () => { - const response = await fetch( - `/api/wallet/nfts/${params.chainId}?owner=${params.walletAddress}`, - ); - return (await response.json()) as WalletNFTApiReturn; + invariant(params.walletAddress, "walletAddress is required"); + return getWalletNFTs({ + chainId: params.chainId, + owner: params.walletAddress, + }); }, enabled: !!params.walletAddress, }); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx index 0f9ea2d37ff..c4e9d6a0e07 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx @@ -25,6 +25,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains"; +import { getAuthToken } from "../../../../api/lib/getAuthToken"; import { StarButton } from "../../components/client/star-button"; import { getChain, getChainMetadata } from "../../utils"; import { AddChainToWallet } from "./components/client/add-chain-to-wallet"; @@ -62,6 +63,7 @@ export default async function ChainPageLayout(props: { const params = await props.params; const { children } = props; const chain = await getChain(params.chain_id); + const authToken = await getAuthToken(); if (params.chain_id !== chain.slug) { redirect(chain.slug); @@ -173,11 +175,13 @@ export default async function ChainPageLayout(props: { {/* Favorite */} - + {authToken && ( + + )} {/* Gas Sponsored badge - Desktop */} {chainMetadata?.gasSponsored && ( diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/[chain_type]/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/[chain_type]/page.tsx index 1a88e1db222..cab118039a2 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/[chain_type]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/[chain_type]/page.tsx @@ -14,6 +14,7 @@ import { import { ChevronDownIcon } from "lucide-react"; import { headers } from "next/headers"; import Link from "next/link"; +import { getAuthToken } from "../../../../api/lib/getAuthToken"; import { AllFilters, ChainOptionsFilter, @@ -56,6 +57,7 @@ export default async function ChainListLayout(props: { params: Promise<{ chain_type: "mainnets" | "testnets" }>; searchParams: Promise; }) { + const authToken = await getAuthToken(); const headersList = await headers(); const viewportWithHint = Number( headersList.get("Sec-Ch-Viewport-Width") || 0, @@ -144,7 +146,11 @@ export default async function ChainListLayout(props: {
{/* we used to have suspense + spinner here, that feels more jarring than the page loading _minutely_ slower */} - + ); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chain-table.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chain-table.tsx index a54b89ca85a..bd6c26cdbed 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chain-table.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chain-table.tsx @@ -140,6 +140,7 @@ async function getChainsToRender(params: SearchParams) { export async function ChainsData(props: { searchParams: SearchParams; activeView: "table" | "grid"; + isLoggedIn: boolean; }) { const { chainsToRender, totalCount, filteredCount } = await getChainsToRender( props.searchParams, @@ -167,8 +168,6 @@ export async function ChainsData(props: { - {/* empty space for the icon */} - Name Chain ID Native Token @@ -188,12 +187,14 @@ export async function ChainsData(props: { .map((c) => c.service)} isDeprecated={chain.status === "deprecated"} favoriteButton={ -
- -
+ props.isLoggedIn ? ( +
+ +
+ ) : undefined } iconUrl={chain.icon?.url} /> @@ -216,12 +217,14 @@ export async function ChainsData(props: { .map((c) => c.service)} isDeprecated={chain.status === "deprecated"} favoriteButton={ -
- -
+ props.isLoggedIn ? ( +
+ +
+ ) : undefined } iconUrl={chain.icon?.url} /> diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx index 3117e152f1d..18568c9a19c 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx @@ -9,7 +9,7 @@ import { getChainMetadata } from "../../../utils"; import type { JSX } from "react"; type ChainListCardProps = { - favoriteButton: JSX.Element; + favoriteButton: JSX.Element | undefined; chainId: number; chainSlug: string; chainName: string; diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx index e91f3c6554e..ed175202cb4 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx @@ -17,7 +17,7 @@ import { getChainMetadata } from "../../../utils"; import type { JSX } from "react"; type ChainListRowProps = { - favoriteButton: JSX.Element; + favoriteButton: JSX.Element | undefined; chainId: number; chainSlug: string; chainName: string; @@ -40,11 +40,11 @@ export async function ChainListRow({ const chainMetadata = await getChainMetadata(chainId); return ( - {favoriteButton} {/* Name */}
+ {favoriteButton &&
{favoriteButton}
} ; }) { + const authToken = await getAuthToken(); const headersList = await headers(); const viewportWithHint = Number( headersList.get("Sec-Ch-Viewport-Width") || 0, @@ -70,7 +72,11 @@ export default async function ChainListPage(props: {
{/* we used to have suspense + spinner here, that feels more jarring than the page loading _minutely_ slower */} - + ); } diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/components/client/star-button.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/components/client/star-button.tsx index c25bdf197dd..ed3a5debb7a 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/components/client/star-button.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/components/client/star-button.tsx @@ -1,44 +1,54 @@ "use client"; +import { apiServerProxy } from "@/actions/proxies"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button, type ButtonProps } from "@/components/ui/button"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { THIRDWEB_API_HOST } from "constants/urls"; import { Star } from "lucide-react"; import { useActiveAccount } from "thirdweb/react"; async function favoriteChains() { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/chains/favorites`, { + const res = await apiServerProxy<{ data: string[] }>({ + pathname: "/v1/chains/favorites", method: "GET", }); - const result = await res.json(); + if (!res.ok) { + throw new Error(res.error); + } - return (result.data ?? []) as string[]; + const result = res.data; + return result.data ?? []; } async function addChainToFavorites(chainId: number) { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/chains/${chainId}/favorite`, - { - method: "POST", - // without body - the API returns 400 - body: JSON.stringify({}), - }, - ); - const result = await res.json(); + const res = await apiServerProxy({ + method: "POST", + body: JSON.stringify({}), + pathname: `/v1/chains/${chainId}/favorite`, + }); + + if (!res.ok) { + throw new Error(res.error); + } + + const result = res.data as { data?: { favorite: boolean } }; return result?.data?.favorite; } async function removeChainFromFavorites(chainId: number) { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/chains/${chainId}/favorite`, - { - method: "DELETE", - }, - ); - const result = await res.json(); + const res = await apiServerProxy<{ data?: { favorite: boolean } }>({ + pathname: `/v1/chains/${chainId}/favorite`, + method: "DELETE", + }); + + if (!res.ok) { + throw new Error(res.error); + } + + const result = res.data; return result?.data?.favorite; } @@ -57,7 +67,6 @@ export function StarButton(props: { iconClassName?: string; variant?: ButtonProps["variant"]; }) { - const address = useActiveAccount()?.address; const queryClient = useQueryClient(); const favChainsQuery = useFavoriteChainIds(); @@ -81,27 +90,31 @@ export function StarButton(props: { const label = isPreferred ? "Remove from Favorites" : "Add to Favorites"; return ( - + + + ); } diff --git a/apps/dashboard/src/app/(landing)/ThemeProvider.tsx b/apps/dashboard/src/app/(landing)/ThemeProvider.tsx new file mode 100644 index 00000000000..5613ec0466f --- /dev/null +++ b/apps/dashboard/src/app/(landing)/ThemeProvider.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { ThemeProvider } from "@/components/theme-provider"; +import { useForceDarkTheme } from "@/components/theme-provider"; +import { ChakraProvider, useColorMode } from "@chakra-ui/react"; +import { Global, css } from "@emotion/react"; +import { useTheme } from "next-themes"; +import { + IBM_Plex_Mono as ibmPlexMonoConstructor, + Inter as interConstructor, +} from "next/font/google"; +import { useEffect } from "react"; +import { generateBreakpointTypographyCssVars } from "tw-components/utils/typography"; +import chakraTheme from "../../theme"; + +const inter = interConstructor({ + subsets: ["latin"], + display: "swap", + fallback: ["system-ui", "Helvetica Neue", "Arial", "sans-serif"], + adjustFontFallback: true, +}); + +const ibmPlexMono = ibmPlexMonoConstructor({ + weight: ["400", "500", "600", "700"], + subsets: ["latin"], + display: "swap", + fallback: ["Consolas", "Courier New", "monospace"], +}); + +const fontSizeCssVars = generateBreakpointTypographyCssVars(); + +const chakraThemeWithFonts = { + ...chakraTheme, + fonts: { + ...chakraTheme.fonts, + heading: inter.style.fontFamily, + body: inter.style.fontFamily, + mono: ibmPlexMono.style.fontFamily, + }, +}; + +export function LandingPageThemeProvider(props: { + children: React.ReactNode; +}) { + return ( + + div > svg { + font-size: 10px !important; + } + `} + /> + + {props.children} + + + + + ); +} + +function TailwindTheme(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} + +const SyncTheme: React.FC = () => { + const { theme, setTheme } = useTheme(); + const { setColorMode } = useColorMode(); + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + setColorMode(theme === "light" ? "light" : "dark"); + }, [setColorMode, theme]); + + // handle dashboard with now old "system" set + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + if (theme === "system") { + setTheme("dark"); + setColorMode("dark"); + } + }, [theme, setTheme, setColorMode]); + + return null; +}; + +function ForceDarkTheme() { + useForceDarkTheme(); + return null; +} diff --git a/apps/dashboard/src/app/(landing)/account-abstraction/page.tsx b/apps/dashboard/src/app/(landing)/account-abstraction/page.tsx new file mode 100644 index 00000000000..e40e5f8168d --- /dev/null +++ b/apps/dashboard/src/app/(landing)/account-abstraction/page.tsx @@ -0,0 +1,207 @@ +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 ( + + + + ), + }, + { + 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: ( + + ), + }, + { + title: "Enterprise-grade security", + description: + "Wallet recovery, 2FA, and multi-signature support for ultimate peace of mind — for users & teams.", + Component: ( + + ), + }, + ]} + /> + + + An all-in-one solution for + + Account Abstraction + + + + Implement account abstraction into any web3 app — with a best-in-class + SDK, full wallet customizability, and managed infrastructure. + + + + + + + + + + + + ); +} diff --git a/apps/dashboard/src/app/(landing)/auth/page.tsx b/apps/dashboard/src/app/(landing)/auth/page.tsx new file mode 100644 index 00000000000..27038fdb2b6 --- /dev/null +++ b/apps/dashboard/src/app/(landing)/auth/page.tsx @@ -0,0 +1,210 @@ +import { Container, Flex } from "@chakra-ui/react"; +import { LandingEndCTA } from "components/landing-pages/end-cta"; +import { LandingGridSection } from "components/landing-pages/grid-section"; +import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase"; +import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image"; +import { LandingIconSectionItem } from "components/landing-pages/icon-section-item"; +import { LandingSectionHeading } from "components/landing-pages/section-heading"; +import type { Metadata } from "next"; +import { Card, TrackedLink } from "tw-components"; +// images +import authIcon from "../../../../public/assets/product-icons/auth.png"; +import iconBuild from "../../../../public/assets/product-pages-icons/wallets/icon-build.svg"; +import iconDataCheck from "../../../../public/assets/product-pages-icons/wallets/icon-data-check.svg"; +import iconEfficient from "../../../../public/assets/product-pages-icons/wallets/icon-efficient.svg"; +import iconPrivate from "../../../../public/assets/product-pages-icons/wallets/icon-private.svg"; +import iconSecure from "../../../../public/assets/product-pages-icons/wallets/icon-secure.svg"; +import iconSimpleClick from "../../../../public/assets/product-pages-icons/wallets/icon-simple-click.svg"; +import iconVerified from "../../../../public/assets/product-pages-icons/wallets/icon-verified.svg"; +import iconWalletManagement from "../../../../public/assets/product-pages-icons/wallets/icon-wallet-management.svg"; +// images +import desktopHeroAuthImage from "../../../../public/assets/product-pages/hero/desktop-hero-auth.png"; +import mobileHeroAuthImage from "../../../../public/assets/product-pages/hero/mobile-hero-auth.png"; +import { getAbsoluteUrl } from "../../../lib/vercel-utils"; + +const TRACKING_CATEGORY = "auth-landing"; + +const GUIDES = [ + { + title: "How to Build a Web3 Creator Platform with a Web2 Backend", + image: + "https://blog.thirdweb.com/content/images/size/w2000/2023/03/How-to-create-a-web3-creator----platform-with-a-web2-backend.png", + link: "https://blog.thirdweb.com/guides/how-to-create-a-web3-creator-platform/", + }, + { + title: "Create An NFT Gated Website", + image: + "https://blog.thirdweb.com/content/images/size/w2000/2022/08/thumbnail-31.png", + link: "https://blog.thirdweb.com/guides/nft-gated-website/", + }, + { + title: "Accept Stripe Subscription Payments For Your Web3 App", + image: + "https://blog.thirdweb.com/content/images/size/w2000/2023/03/Add-stripe-subscriptions--with-web3-auth-2.png", + link: "https://blog.thirdweb.com/guides/add-stripe-subscriptions-with-web3-auth/", + }, +]; + +const title = "The Complete Toolkit for Web3 Authentication"; +const description = + "Auth for the most popular web3 wallets & web2 login flows. Verify your users' identities & prove wallet ownership to off-chain systems."; + +export const metadata: Metadata = { + title, + description, + openGraph: { + title, + description, + images: [ + { + url: `${getAbsoluteUrl()}/assets/og-image/auth.png`, + width: 1200, + height: 630, + }, + ], + }, +}; + +export default function Page() { + return ( + + + + + + + + + + + + Built on the SIWE ( + + Sign-in with Ethereum + + ) standard. Securely verify a user's on-chain identity, + without relying on a centralized database to verify their + identity. + + } + /> + + + + + + + Secure your backend with a web3-compatible authentication system + compliant with the widely used{" "} + + JSON Web Token + {" "} + standard. + + } + /> + + + + + } + > + + + + + + + + + + + + + + + + ); +} diff --git a/apps/dashboard/src/app/(landing)/in-app-wallets/page.tsx b/apps/dashboard/src/app/(landing)/in-app-wallets/page.tsx new file mode 100644 index 00000000000..d29e6112ffe --- /dev/null +++ b/apps/dashboard/src/app/(landing)/in-app-wallets/page.tsx @@ -0,0 +1,272 @@ +import { Container, Flex } from "@chakra-ui/react"; +import { ChakraNextImage } from "components/Image"; +import { LandingCardWithImage } from "components/landing-pages/card-with-image"; +import { LandingDynamicSelector } from "components/landing-pages/dynamic-selector"; +import { LandingEndCTA } from "components/landing-pages/end-cta"; +import { LandingGridSection } from "components/landing-pages/grid-section"; +import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase"; +import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image"; +import { getAbsoluteUrl } from "lib/vercel-utils"; +import type { Metadata } from "next"; +import { Heading } from "tw-components"; +// images +import analyticsImage from "../../../../public/assets/landingpage/desktop/analytics.png"; +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"; +import siweImage from "../../../../public/assets/landingpage/desktop/siwe.png"; +import walletImage from "../../../../public/assets/landingpage/desktop/wallet.png"; +import analyticsMobileImage from "../../../../public/assets/landingpage/mobile/analytics.png"; +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"; +import siweMobileImage from "../../../../public/assets/landingpage/mobile/siwe.png"; +import walletMobileImage from "../../../../public/assets/landingpage/mobile/wallet.png"; +import embeddedWalletIcon from "../../../../public/assets/product-icons/embedded-wallet.png"; +import authImage from "../../../../public/assets/product-pages/embedded-wallets/auth.png"; +import crossPlatformImage from "../../../../public/assets/product-pages/embedded-wallets/cross-platform.png"; +import embeddedWalletImage from "../../../../public/assets/product-pages/embedded-wallets/embedded-wallet.png"; +import paperImage from "../../../../public/assets/product-pages/embedded-wallets/paper.png"; +import seamlessImage from "../../../../public/assets/product-pages/embedded-wallets/seamless.png"; +import desktopHeroEmbeddedWalletsImage from "../../../../public/assets/product-pages/hero/desktop-hero-embedded-wallets.png"; +import mobileHeroEmbeddedWalletsImage from "../../../../public/assets/product-pages/hero/mobile-hero-embedded-wallets.png"; +import getStartedImage from "../../../../public/assets/product-pages/smart-wallet/get-started.png"; + +const TRACKING_CATEGORY = "embedded-wallets-landing"; + +const GUIDES = [ + { + title: "Docs: In-App Wallets Overview", + image: embeddedWalletImage, + link: "https://portal.thirdweb.com/connect/in-app-wallet/overview", + }, + { + title: "Live Demo: In-App Wallets", + image: paperImage, + link: "https://catattack.thirdweb.com", + }, + { + title: "Quick-Start Template: In-App Wallet + Account Abstraction", + image: getStartedImage, + link: "https://github.com/thirdweb-example/embedded-smart-wallet", + }, +]; + +const title = "In-App Wallets: Onboard Everyone to your App"; +const description = + "Onboard anyone with an email or Google account—with 1-click login flows, flexible auth options, & secure account recovery. Learn more."; + +export const metadata: Metadata = { + title, + description, + openGraph: { + title, + description, + images: [ + { + url: `${getAbsoluteUrl()}/assets/og-image/embedded-wallets.png`, + width: 1200, + height: 630, + }, + ], + }, +}; + +export default function Page() { + return ( + + + + + ), + }, + { + title: "Integrate with your own custom auth", + description: + "Spin up in-app wallets for your users with your app or game's existing auth system.", + Component: ( + + ), + }, + { + title: "Cross-platform support", + description: + "Enable users to log into their accounts (and access their wallets) from any device, in one click. Support for web, mobile, & Unity.", + Component: ( + + ), + }, + ]} + /> + + + + Abstract away complexity for your users + +
+ } + > + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/dashboard/src/app/(landing)/layout.tsx b/apps/dashboard/src/app/(landing)/layout.tsx new file mode 100644 index 00000000000..c43a80acff7 --- /dev/null +++ b/apps/dashboard/src/app/(landing)/layout.tsx @@ -0,0 +1,13 @@ +import type React from "react"; +import { LandingLayout } from "../../components/landing-pages/layout"; +import { LandingPageThemeProvider } from "./ThemeProvider"; + +export default function Layout(props: { + children: React.ReactNode; +}) { + return ( + + {props.children} + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx index a9317dcd5c3..3713f5a9cc3 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx @@ -8,7 +8,7 @@ export function EcosystemPermissionsPage({ params, authToken, }: { params: { slug: string }; authToken: string }) { - const { ecosystem } = useEcosystem({ slug: params.slug }); + const { data: ecosystem } = useEcosystem({ slug: params.slug }); return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts index b86a7ce9af6..7cbd47cb15b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts @@ -1,6 +1,5 @@ +import { apiServerProxy } from "@/actions/proxies"; import { useQuery } from "@tanstack/react-query"; -import { THIRDWEB_API_HOST } from "constants/urls"; -import { FetchError } from "utils/error"; import type { Ecosystem } from "../../../types"; export function useEcosystem({ @@ -14,23 +13,19 @@ export function useEcosystem({ refetchOnWindowFocus?: boolean; initialData?: Ecosystem; }) { - const ecosystemQuery = useQuery({ + return useQuery({ queryKey: ["ecosystems", slug], queryFn: async () => { - const res = await fetch( - `${THIRDWEB_API_HOST}/v1/ecosystem-wallet/${slug}`, - ); + const res = await apiServerProxy({ + pathname: `/v1/ecosystem-wallet/${slug}`, + method: "GET", + }); if (!res.ok) { - const data = await res.json(); - console.error(data); - throw new FetchError( - res, - data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems", - ); + throw new Error(res.error); } - const data = (await res.json()) as { result: Ecosystem }; + const data = res.data as { result: Ecosystem }; return data.result; }, retry: false, @@ -38,10 +33,4 @@ export function useEcosystem({ refetchOnWindowFocus, initialData, }); - - return { - ...ecosystemQuery, - error: ecosystemQuery.error as FetchError | undefined, - ecosystem: ecosystemQuery.data, - }; } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts index 9a9b996a203..c11289e8912 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts @@ -1,30 +1,25 @@ +import { apiServerProxy } from "@/actions/proxies"; import { useQuery } from "@tanstack/react-query"; -import { THIRDWEB_API_HOST } from "constants/urls"; import { useActiveAccount } from "thirdweb/react"; import type { Ecosystem } from "../types"; export function useEcosystemList() { const address = useActiveAccount()?.address; - const ecosystemQuery = useQuery({ + return useQuery({ queryKey: ["ecosystems", address], queryFn: async () => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/ecosystem-wallet/list`); + const res = await apiServerProxy({ + pathname: "/v1/ecosystem-wallet/list", + method: "GET", + }); if (!res.ok) { - const data = await res.json(); - console.error(data); - throw new Error(data?.error?.message ?? "Failed to fetch ecosystems"); + throw new Error(res.error ?? "Failed to fetch ecosystems"); } - const data = (await res.json()) as { result: Ecosystem[] }; + const data = res.data as { result: Ecosystem[] }; return data.result; }, retry: false, }); - - return { - ...ecosystemQuery, - isPending: ecosystemQuery.isPending, - ecosystems: (ecosystemQuery.data ?? []) satisfies Ecosystem[], - }; } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/import/EngineImportPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/import/EngineImportPage.tsx index f984249b3ea..2d76770c47d 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/import/EngineImportPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/import/EngineImportPage.tsx @@ -1,11 +1,13 @@ "use client"; +import { apiServerProxy } from "@/actions/proxies"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { TrackedLinkTW } from "@/components/ui/tracked-link"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { FormControl } from "@chakra-ui/react"; -import { THIRDWEB_API_HOST } from "constants/urls"; +import { useMutation } from "@tanstack/react-query"; import { CircleAlertIcon, CloudDownloadIcon, @@ -34,12 +36,13 @@ export const EngineImportPage = (props: { }, }); - const onSubmit = async (data: ImportEngineInput) => { - try { + const importMutation = useMutation({ + mutationFn: async (data: ImportEngineInput) => { // Instance URLs should end with a /. const url = data.url.endsWith("/") ? data.url : `${data.url}/`; - const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, { + const res = await apiServerProxy({ + pathname: "/v1/engine", method: "POST", headers: { "Content-Type": "application/json", @@ -49,10 +52,16 @@ export const EngineImportPage = (props: { url, }), }); + if (!res.ok) { - throw new Error(`Unexpected status ${res.status}`); + throw new Error(res.error); } + }, + }); + const onSubmit = async (data: ImportEngineInput) => { + try { + await importMutation.mutateAsync(data); toast.success("Engine imported successfully"); router.push(`/team/${props.teamSlug}/~/engine`); } catch { @@ -128,7 +137,11 @@ export const EngineImportPage = (props: { variant="primary" className="w-full gap-2 text-base" > - + {importMutation.isPending ? ( + + ) : ( + + )} Import diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer.tsx index 15690bb38d3..0b6b9f8dc58 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer.tsx @@ -18,7 +18,7 @@ function ViewDocs(props: { }) { const TRACKING_CATEGORY = props.trackingCategory; return ( -
+

View Docs

@@ -130,7 +130,7 @@ function Templates(props: { trackingCategory: string; }) { return ( -
+

Relevant Templates

diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/components/webhooks.client.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/components/webhooks.client.tsx index cc8afb98123..532c181fcbc 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/components/webhooks.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/components/webhooks.client.tsx @@ -1,7 +1,9 @@ "use client"; +import { payServerProxy } from "@/actions/proxies"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -65,16 +67,23 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) { const webhooksQuery = useQuery({ queryKey: ["webhooks", props.clientId], queryFn: async () => { - const res = await fetch( - `/api/server-proxy/pay/webhooks/get-all?clientId=${props.clientId}`, - { - headers: { - "Content-Type": "application/json", - }, + const res = await payServerProxy({ + method: "GET", + pathname: "/webhooks/get-all", + searchParams: { + clientId: props.clientId, }, - ); - const json = await res.json(); - return json.result as Array; + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + throw new Error(); + } + + const json = res.data as { result: Array }; + return json.result; }, }); @@ -97,61 +106,68 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) { } return ( - -
- - - Label - Url - Secret - Created - - - - - - - - - {webhooksQuery.data.map((webhook) => ( - - {webhook.label} - {webhook.url} - - - - - {formatDistanceToNow(webhook.createdAt, { addSuffix: true })} - - - - - - +
+
+

Webhooks

+ + + +
+ +
+ + +
+ + + Label + Url + Secret + Created + Delete - ))} - -
- + + + {webhooksQuery.data.map((webhook) => ( + + {webhook.label} + {webhook.url} + + + + + {formatDistanceToNow(webhook.createdAt, { addSuffix: true })} + + + + + + + + ))} + + + +
); } const formSchema = z.object({ url: z.string().url("Please enter a valid URL."), - label: z.string().min(1, "Please enter a label."), + label: z.string().min(3, "Label must be at least 3 characters long"), }); function CreateWebhookButton(props: PropsWithChildren) { @@ -166,15 +182,21 @@ function CreateWebhookButton(props: PropsWithChildren) { const queryClient = useQueryClient(); const createMutation = useMutation({ mutationFn: async (values: z.infer) => { - const res = await fetch("/api/server-proxy/pay/webhooks/create", { + const res = await payServerProxy({ method: "POST", + pathname: "/webhooks/create", + body: JSON.stringify({ ...values, clientId: props.clientId }), headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ ...values, clientId: props.clientId }), }); - const json = await res.json(); - return json.result as string; + + if (!res.ok) { + throw new Error(res.error); + } + + const json = res.data as { result: string }; + return json.result; }, onSuccess: () => { return queryClient.invalidateQueries({ @@ -189,26 +211,21 @@ function CreateWebhookButton(props: PropsWithChildren) {
- toast.promise( - createMutation.mutateAsync(values, { - onError: (err) => - toast.error("Failed to create webhook", { - description: (err as Error).message, - }), - onSuccess: () => { - setOpen(false); - form.reset(); - form.clearErrors(); - form.setValue("url", ""); - form.setValue("label", ""); - }, - }), - { - loading: "Creating webhook...", - success: "Webhook created", - error: "Failed to create webhook", + createMutation.mutateAsync(values, { + onError: (err) => { + toast.error("Failed to create webhook", { + description: err instanceof Error ? err.message : undefined, + }); + }, + onSuccess: () => { + setOpen(false); + toast.success("Webhook created successfully"); + form.reset(); + form.clearErrors(); + form.setValue("url", ""); + form.setValue("label", ""); }, - ), + }), )} className="flex flex-col gap-4" > @@ -266,8 +283,13 @@ function CreateWebhookButton(props: PropsWithChildren) { - @@ -284,15 +306,21 @@ function DeleteWebhookButton( const queryClient = useQueryClient(); const deleteMutation = useMutation({ mutationFn: async (id: string) => { - const res = await fetch("/api/server-proxy/pay/webhooks/revoke", { + const res = await payServerProxy({ method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id, clientId: props.clientId }), + pathname: "/webhooks/revoke", }); - const json = await res.json(); - return json.result as string; + + if (!res.ok) { + throw new Error("Failed to delete webhook"); + } + + const json = res.data as { result: string }; + return json.result; }, onSuccess: () => { return queryClient.invalidateQueries({ @@ -314,24 +342,28 @@ function DeleteWebhookButton( diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx index 44ce7556077..ddb734706ed 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx @@ -114,7 +114,7 @@ async function ProjectAnalytics(props: { inAppWalletUsage, userOpUsageTimeSeries, userOpUsage, - ] = await Promise.all([ + ] = await Promise.allSettled([ // Aggregated wallet connections getWalletConnections({ clientId: project.publishableKey, @@ -153,7 +153,8 @@ async function ProjectAnalytics(props: { return (
- {walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? ( + {walletUserStatsTimeSeries.status === "fulfilled" && + walletUserStatsTimeSeries.value.some((w) => w.totalUsers !== 0) ? (
@@ -180,16 +181,18 @@ async function ProjectAnalytics(props: { clientId={project.publishableKey} />
- {walletConnections.length > 0 ? ( - + {walletConnections.status === "fulfilled" && + walletConnections.value.length > 0 ? ( + ) : ( )} - {inAppWalletUsage.length > 0 ? ( - + {inAppWalletUsage.status === "fulfilled" && + inAppWalletUsage.value.length > 0 ? ( + ) : ( )}
- {userOpUsage.length > 0 ? ( + {userOpUsageTimeSeries.status === "fulfilled" && + userOpUsage.status === "fulfilled" && + userOpUsage.value.length > 0 ? (
) : ( diff --git a/apps/dashboard/src/components/Image/index.tsx b/apps/dashboard/src/components/Image/index.tsx index c135a5fb32e..b8ddfd026db 100644 --- a/apps/dashboard/src/components/Image/index.tsx +++ b/apps/dashboard/src/components/Image/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import { chakra } from "@chakra-ui/react"; import NextImage from "next/image"; diff --git a/apps/dashboard/src/components/analytics/stat.tsx b/apps/dashboard/src/components/analytics/stat.tsx index d65978013f7..142f7d5245d 100644 --- a/apps/dashboard/src/components/analytics/stat.tsx +++ b/apps/dashboard/src/components/analytics/stat.tsx @@ -5,7 +5,7 @@ export const Stat: React.FC<{ formatter?: (value: number) => string; }> = ({ label, value, formatter, icon: Icon }) => { return ( -
+
{value !== undefined && formatter diff --git a/apps/dashboard/src/components/buttons/MismatchButton.tsx b/apps/dashboard/src/components/buttons/MismatchButton.tsx index d398d00ec01..53b0f6321d9 100644 --- a/apps/dashboard/src/components/buttons/MismatchButton.tsx +++ b/apps/dashboard/src/components/buttons/MismatchButton.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiServerProxy } from "@/actions/proxies"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; @@ -47,7 +48,6 @@ import { } from "thirdweb/react"; import { privateKeyToAccount } from "thirdweb/wallets"; import { getFaucetClaimAmount } from "../../app/api/testnet-faucet/claim/claim-amount"; -import { THIRDWEB_API_HOST } from "../../constants/urls"; import { useAllChainsData } from "../../hooks/chains/allChains"; import { useV5DashboardChain } from "../../lib/v5-adapter"; @@ -297,18 +297,24 @@ function NoFundsDialogContent(props: { const chainWithServiceInfoQuery = useQuery({ queryKey: ["chain-with-services", props.chain.id], queryFn: async () => { - const [chain, chainServices] = await Promise.all([ - fetch(`${THIRDWEB_API_HOST}/v1/chains/${props.chain.id}`).then((res) => - res.json(), - ) as Promise<{ data: ChainMetadata }>, - fetch(`${THIRDWEB_API_HOST}/v1/chains/${props.chain.id}/services`).then( - (res) => res.json(), - ) as Promise<{ data: ChainServices }>, + const [chainRes, chainServicesRes] = await Promise.all([ + apiServerProxy<{ data: ChainMetadata }>({ + pathname: `/v1/chains/${props.chain.id}`, + method: "GET", + }), + apiServerProxy<{ data: ChainServices }>({ + pathname: `/v1/chains/${props.chain.id}/services`, + method: "GET", + }), ]); + if (!chainRes.ok || !chainServicesRes.ok) { + throw new Error("Failed to fetch chain with services"); + } + return { - ...chain.data, - services: chainServices.data.services, + ...chainRes.data.data, + services: chainServicesRes.data.data.services, } satisfies ChainMetadataWithServices; }, enabled: !!props.chain.id, diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx index 55d71489d41..394f8c3c5d4 100644 --- a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx @@ -114,7 +114,7 @@ export function InAppWalletUsersChartCardUI(props: { chartData.every((data) => data.sponsoredUsd === 0); return ( -
+

{props.title}

diff --git a/apps/dashboard/src/components/homepage/sections/NewsletterSection.tsx b/apps/dashboard/src/components/homepage/sections/NewsletterSection.tsx index 96a9aa88b8b..28342633097 100644 --- a/apps/dashboard/src/components/homepage/sections/NewsletterSection.tsx +++ b/apps/dashboard/src/components/homepage/sections/NewsletterSection.tsx @@ -1,6 +1,10 @@ +"use client"; + +import { emailSignup } from "@/actions/emailSignup"; import { Box, Container, Flex, FormControl, Input } from "@chakra-ui/react"; import { MailCheckIcon } from "lucide-react"; import { useState } from "react"; +import { toast } from "sonner"; import { Button, Text } from "tw-components"; export const NewsletterSection = () => { @@ -17,12 +21,17 @@ export const NewsletterSection = () => { setIsSubmitting(true); try { - await fetch("/api/email-signup", { - method: "POST", - body: JSON.stringify({ email }), + const res = await emailSignup({ + email, }); + + if (res.status.toString().startsWith("2")) { + toast.success("Successfully signed up for our newsletter!"); + } + setEmail(""); } catch (err) { + toast.error("Failed to sign up for our newsletter"); console.error(err); } diff --git a/apps/dashboard/src/components/landing-pages/dynamic-selector.tsx b/apps/dashboard/src/components/landing-pages/dynamic-selector.tsx index db4fca9ffc5..4bd100f1afb 100644 --- a/apps/dashboard/src/components/landing-pages/dynamic-selector.tsx +++ b/apps/dashboard/src/components/landing-pages/dynamic-selector.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Box, Flex, GridItem, SimpleGrid } from "@chakra-ui/react"; import { useTrack } from "hooks/analytics/useTrack"; import { type ReactElement, useState } from "react"; diff --git a/apps/dashboard/src/components/landing-pages/guide-showcase.tsx b/apps/dashboard/src/components/landing-pages/guide-showcase.tsx index 336539e4b2b..5cc76be0336 100644 --- a/apps/dashboard/src/components/landing-pages/guide-showcase.tsx +++ b/apps/dashboard/src/components/landing-pages/guide-showcase.tsx @@ -1,12 +1,13 @@ import { Flex, SimpleGrid } from "@chakra-ui/react"; import { GuideCard } from "components/product-pages/common/GuideCard"; import { MoveRightIcon } from "lucide-react"; +import type { StaticImageData } from "next/image"; import { Heading, TrackedLink, type TrackedLinkProps } from "tw-components"; type BlogPost = { title: string; description?: string; - image: string; + image: string | StaticImageData; link: string; }; diff --git a/apps/dashboard/src/components/landing-pages/layout.tsx b/apps/dashboard/src/components/landing-pages/layout.tsx index 600bacba143..ebd204bc250 100644 --- a/apps/dashboard/src/components/landing-pages/layout.tsx +++ b/apps/dashboard/src/components/landing-pages/layout.tsx @@ -1,28 +1,21 @@ -import { useForceDarkTheme } from "@/components/theme-provider"; import { Box, type BoxProps, Flex } from "@chakra-ui/react"; import { HomepageFooter } from "components/footer/Footer"; import { NewsletterSection } from "components/homepage/sections/NewsletterSection"; import { HomepageTopNav } from "components/product-pages/common/Topnav"; -import { NextSeo, type NextSeoProps } from "next-seo"; import type { ComponentWithChildren } from "types/component-with-children"; interface LandingLayoutProps { - seo: NextSeoProps; bgColor?: string; py?: BoxProps["py"]; } export const LandingLayout: ComponentWithChildren = ({ - seo, bgColor = "#000", children, py, }) => { - useForceDarkTheme(); - return ( <> - = ({ }); try { - const response = await fetch("/api/apply-op-sponsorship", { - method: "POST", - body: JSON.stringify({ fields }), + const response = await applyOpSponsorship({ + fields, }); if (!response.ok) { @@ -113,8 +113,6 @@ export const ApplyForOpCreditsForm: React.FC = ({ throw new Error("Form submission failed"); } - await response.json(); - trackEvent({ category: "op-sponsorship", action: "apply", diff --git a/apps/dashboard/src/components/onboarding/applyOpSponsorship.ts b/apps/dashboard/src/components/onboarding/applyOpSponsorship.ts new file mode 100644 index 00000000000..c4c67b14f5f --- /dev/null +++ b/apps/dashboard/src/components/onboarding/applyOpSponsorship.ts @@ -0,0 +1,42 @@ +"use server"; + +export async function applyOpSponsorship(params: { + fields: { + name: string; + // biome-ignore lint/suspicious/noExplicitAny: FIXME + value: any; + }[]; +}) { + const { fields } = params; + + if (!process.env.HUBSPOT_ACCESS_TOKEN) { + return { + error: "missing HUBSPOT_ACCESS_TOKEN", + ok: false, + }; + } + + const response = await fetch( + "https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/2fbf6a3b-d4cc-4a23-a4f5-42674e8487b9", + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`, + }, + method: "POST", + body: JSON.stringify({ fields }), + }, + ); + + if (!response.ok) { + const errorMessage = await response.text(); + return { + error: errorMessage, + ok: response.ok, + }; + } + + return { + ok: response.ok, + }; +} diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx index e085728df01..6d5263993b9 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx @@ -65,12 +65,7 @@ export function TableData({ children }: { children: React.ReactNode }) { } export function TableHeadingRow({ children }: { children: React.ReactNode }) { - return ( - - {children} -
- - ); + return {children}; } export function TableHeading(props: { children: React.ReactNode }) { diff --git a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayCustomers.ts b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayCustomers.ts index ef5e570481a..93d2d2f43bc 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayCustomers.ts +++ b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayCustomers.ts @@ -1,3 +1,4 @@ +import { payServerProxy } from "@/actions/proxies"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; @@ -31,31 +32,25 @@ export function usePayCustomers(options: { ? "/stats/new-customers/v1" : "/stats/customers/v1"; - const searchParams = new URLSearchParams(); - const start = options.pageSize * pageParam; - searchParams.append("skip", `${start}`); - searchParams.append("take", `${options.pageSize}`); - - searchParams.append("clientId", options.clientId); - searchParams.append("fromDate", `${options.from.getTime()}`); - searchParams.append("toDate", `${options.to.getTime()}`); - const res = await fetch( - `/api/server-proxy/pay/${endpoint}?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + const res = await payServerProxy({ + method: "GET", + pathname: endpoint, + searchParams: { + skip: `${start}`, + take: `${options.pageSize}`, + clientId: options.clientId, + fromDate: `${options.from.getTime()}`, + toDate: `${options.to.getTime()}`, }, - ); + }); if (!res.ok) { throw new Error("Failed to fetch pay volume"); } - const resJSON = (await res.json()) as Response; + const resJSON = res.data as Response; const pageData = resJSON.result.data; const itemsRequested = options.pageSize * (pageParam + 1); diff --git a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayNewCustomers.ts b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayNewCustomers.ts index 5f5445c7c6c..f359e3241cc 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayNewCustomers.ts +++ b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayNewCustomers.ts @@ -1,3 +1,4 @@ +import { payServerProxy } from "@/actions/proxies"; import { useQuery } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; @@ -33,27 +34,25 @@ export function usePayNewCustomers(options: { return useQuery({ queryKey: ["usePayNewCustomers", address, options], queryFn: async () => { - const searchParams = new URLSearchParams(); - searchParams.append("intervalType", options.intervalType); - searchParams.append("clientId", options.clientId); - searchParams.append("fromDate", `${options.from.getTime()}`); - searchParams.append("toDate", `${options.to.getTime()}`); - - const res = await fetch( - `/api/server-proxy/pay/stats/aggregate/customers/v1?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + const res = await payServerProxy({ + pathname: "/stats/aggregate/customers/v1", + searchParams: { + intervalType: options.intervalType, + clientId: options.clientId, + fromDate: `${options.from.getTime()}`, + toDate: `${options.to.getTime()}`, + }, + method: "GET", + headers: { + "Content-Type": "application/json", }, - ); + }); if (!res.ok) { throw new Error("Failed to fetch new customers"); } - const resJSON = (await res.json()) as Response; + const resJSON = res.data as Response; return resJSON.result.data; }, diff --git a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayPurchases.ts b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayPurchases.ts index 3834f9588bd..c7796431be1 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayPurchases.ts +++ b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayPurchases.ts @@ -1,3 +1,4 @@ +import { payServerProxy } from "@/actions/proxies"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; @@ -71,29 +72,26 @@ export function usePayPurchases(options: PayPurchaseOptions) { } export async function getPayPurchases(options: PayPurchaseOptions) { - const searchParams = new URLSearchParams(); - searchParams.append("skip", `${options.start}`); - searchParams.append("take", `${options.count}`); - - searchParams.append("clientId", options.clientId); - searchParams.append("fromDate", `${options.from.getTime()}`); - searchParams.append("toDate", `${options.to.getTime()}`); - - const res = await fetch( - `/api/server-proxy/pay/stats/purchases/v1?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + const res = await payServerProxy({ + pathname: "/stats/purchases/v1", + searchParams: { + skip: `${options.start}`, + take: `${options.count}`, + clientId: options.clientId, + fromDate: `${options.from.getTime()}`, + toDate: `${options.to.getTime()}`, }, - ); + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); if (!res.ok) { throw new Error("Failed to fetch pay volume"); } - const resJSON = (await res.json()) as Response; + const resJSON = res.data as Response; return resJSON.result.data; } diff --git a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayVolume.ts b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayVolume.ts index 3009972ee82..b767e5719be 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayVolume.ts +++ b/apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayVolume.ts @@ -1,3 +1,4 @@ +import { payServerProxy } from "@/actions/proxies"; import { useQuery } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; @@ -64,29 +65,27 @@ export function usePayVolume(options: { return useQuery({ queryKey: ["usePayVolume", address, options], queryFn: async () => { - const searchParams = new URLSearchParams(); - searchParams.append("intervalType", options.intervalType); - searchParams.append("clientId", options.clientId); - searchParams.append("fromDate", `${options.from.getTime()}`); - searchParams.append("toDate", `${options.to.getTime()}`); - - const res = await fetch( - `/api/server-proxy/pay/stats/aggregate/volume/v1?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + const res = await payServerProxy({ + pathname: "/stats/aggregate/volume/v1", + searchParams: { + intervalType: options.intervalType, + clientId: options.clientId, + fromDate: `${options.from.getTime()}`, + toDate: `${options.to.getTime()}`, + }, + headers: { + "Content-Type": "application/json", }, - ); + method: "GET", + }); if (!res.ok) { throw new Error("Failed to fetch pay volume"); } - const resJSON = (await res.json()) as Response; + const json = res.data as Response; - return resJSON.result.data; + return json.result.data; }, retry: false, }); diff --git a/apps/dashboard/src/components/product-pages/common/GuideCard.tsx b/apps/dashboard/src/components/product-pages/common/GuideCard.tsx index a67090c3115..6c61eb2835c 100644 --- a/apps/dashboard/src/components/product-pages/common/GuideCard.tsx +++ b/apps/dashboard/src/components/product-pages/common/GuideCard.tsx @@ -1,5 +1,5 @@ import { Box, Flex } from "@chakra-ui/react"; -import NextImage from "next/image"; +import NextImage, { type StaticImageData } from "next/image"; import { Heading, Text, @@ -9,7 +9,7 @@ import { interface GuideCardProps extends Pick { - image: string; + image: string | StaticImageData; title: string; description?: string; link: string; diff --git a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx index d6aefc7fa5e..925c262d9bc 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiServerProxy } from "@/actions/proxies"; import { DangerSettingCard } from "@/components/blocks/DangerSettingCard"; import { SettingsCard } from "@/components/blocks/SettingsCard"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -63,8 +64,9 @@ function ApplyCouponCard(props: { scrollIntoView={!!couponCode} isPaymentSetup={props.isPaymentSetup} submit={async (promoCode: string) => { - const res = await fetch("/api/server-proxy/api/v1/coupons/redeem", { + const res = await apiServerProxy<{ data: ActiveCouponResponse }>({ method: "POST", + pathname: "/v1/coupons/redeem", headers: { "Content-Type": "application/json", }, @@ -75,10 +77,10 @@ function ApplyCouponCard(props: { }); if (res.ok) { - const json = await res.json(); + const json = res.data; return { status: 200, - data: json.data as ActiveCouponResponse, + data: json.data, }; } @@ -111,7 +113,7 @@ export function ApplyCouponCardUI(props: { const form = useForm>({ resolver: zodResolver(couponFormSchema), defaultValues: { - promoCode: props.prefillPromoCode, + promoCode: props.prefillPromoCode || "", }, }); @@ -297,29 +299,28 @@ export function CouponSection(props: { const activeCoupon = useQuery({ queryKey: ["active-coupon", address, props.teamId], queryFn: async () => { - const res = await fetch( - `/api/server-proxy/api/v1/active-coupon${ - props.teamId ? `?teamId=${props.teamId}` : "" - }`, - ); + const res = await apiServerProxy<{ data: ActiveCouponResponse }>({ + method: "GET", + pathname: "/v1/active-coupon", + searchParams: props.teamId ? { teamId: props.teamId } : undefined, + }); + if (!res.ok) { return null; } - const json = await res.json(); - return json.data as ActiveCouponResponse; + const json = res.data; + return json.data; }, }); const deleteActiveCoupon = useMutation({ mutationFn: async () => { - const res = await fetch( - `/api/server-proxy/api/v1/active-coupon${ - props.teamId ? `?teamId=${props.teamId}` : "" - }`, - { - method: "DELETE", - }, - ); + const res = await apiServerProxy({ + method: "DELETE", + pathname: "/v1/active-coupon", + searchParams: props.teamId ? { teamId: props.teamId } : undefined, + }); + if (!res.ok) { throw new Error("Failed to delete coupon"); } diff --git a/apps/dashboard/src/components/shared/ProgressBar.tsx b/apps/dashboard/src/components/shared/ProgressBar.tsx deleted file mode 100644 index bc04b65a654..00000000000 --- a/apps/dashboard/src/components/shared/ProgressBar.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; - -// Modified code from https://github.com/MatinAniss/traction.js - -interface ProgressBarProps { - color: string; - incrementInterval: number; - incrementAmount: number; - transitionDuration: number; - transitionTimingFunction: - | "ease" - | "linear" - | "ease-in" - | "ease-out" - | "ease-in-out"; -} - -const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export const ProgressBar: React.FC = (props) => { - // Declare states - const router = useRouter(); - const [progress, setProgress] = useState(0); - const [isVisible, setIsVisible] = useState(false); - - // Progress bar inline styling - const styling = { - position: "fixed", - top: 0, - left: 0, - width: `${progress}%`, - height: "2px", - backgroundColor: props.color, - transition: `width ${props.transitionDuration}ms ${props.transitionTimingFunction}`, - opacity: isVisible ? 1 : 0, - zIndex: 9999999999, - } as React.CSSProperties; - - // somewhat legitimate use-case - // TODO: do we really need this? - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - // Declare timeout - let status: "in-progress" | "idle" = "idle"; - let intervalId: ReturnType; - - // Route change start function - const onRouteChangeStart = async () => { - status = "in-progress"; - - // only show progress bar if it takes longer than 200ms for the page to load - await wait(200); - - if (status !== "in-progress") { - return; - } - - setIsVisible(true); - setProgress(props.incrementAmount); - // clear any existing interval - clearInterval(intervalId); - - const newIntervalId = setInterval(() => { - if (status === "idle") { - clearInterval(newIntervalId); - return; - } - setProgress((_progress) => { - return Math.min(_progress + props.incrementAmount, 90); - }); - }, props.incrementInterval); - - intervalId = newIntervalId; - }; - - // Route change complete function - const onRouteChangeComplete = () => { - status = "idle"; - clearInterval(intervalId); - setProgress(100); - setTimeout(() => { - if (status === "idle") { - setIsVisible(false); - setProgress(0); - } - }, props.transitionDuration); - }; - - // Route change error function - const onRouteChangeError = () => { - status = "idle"; - clearInterval(intervalId); - setIsVisible(false); - setProgress(0); - }; - - // Router event listeners - router.events.on("routeChangeStart", onRouteChangeStart); - router.events.on("routeChangeComplete", onRouteChangeComplete); - router.events.on("routeChangeError", onRouteChangeError); - - return () => { - router.events.off("routeChangeStart", onRouteChangeStart); - router.events.off("routeChangeComplete", onRouteChangeComplete); - router.events.off("routeChangeError", onRouteChangeError); - clearInterval(intervalId); - }; - }, [ - props.incrementAmount, - props.incrementInterval, - props.transitionDuration, - router.events, - ]); - - return
; -}; diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx index 608d72863c1..f3cfe56cecd 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx @@ -111,7 +111,7 @@ export function SponsoredTransactionsChartCard(props: { chartData.every((data) => data.transactions === 0); return ( -
+

Sponsored Transactions

diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx index 33d0efa3ad5..2a24eea67e0 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx @@ -111,7 +111,7 @@ export function TotalSponsoredChartCard(props: { chartData.every((data) => data.sponsoredUsd === 0); return ( -
+

Gas Sponsored

diff --git a/apps/dashboard/src/constants/urls.ts b/apps/dashboard/src/constants/urls.ts index 8f9db171323..aca7f7a9eb2 100644 --- a/apps/dashboard/src/constants/urls.ts +++ b/apps/dashboard/src/constants/urls.ts @@ -1,7 +1,3 @@ -export const THIRDWEB_API_HOST = "/api/server-proxy/api"; - -export const THIRDWEB_ANALYTICS_API_HOST = "/api/server-proxy/analytics"; - export const THIRDWEB_EWS_API_HOST = process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST || "https://in-app-wallet.thirdweb.com"; diff --git a/apps/dashboard/src/data/analytics/fetch-api-server.ts b/apps/dashboard/src/data/analytics/fetch-api-server.ts index cb2fb4635d9..82baaf9b93e 100644 --- a/apps/dashboard/src/data/analytics/fetch-api-server.ts +++ b/apps/dashboard/src/data/analytics/fetch-api-server.ts @@ -1,4 +1,5 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; +import { API_SERVER_URL } from "@/constants/env"; import { cookies } from "next/headers"; import { getAddress } from "thirdweb"; import "server-only"; @@ -19,22 +20,18 @@ export async function fetchApiServer( } // create a new URL object for the analytics server - const API_SERVER_URL = new URL( - process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com", - ); - API_SERVER_URL.pathname = pathname; + const url = new URL(API_SERVER_URL); + + url.pathname = pathname; for (const param of searchParams?.split("&") || []) { const [key, value] = param.split("="); if (!key || !value) { throw new Error("Invalid input, no key or value provided"); } - API_SERVER_URL.searchParams.append( - decodeURIComponent(key), - decodeURIComponent(value), - ); + url.searchParams.append(decodeURIComponent(key), decodeURIComponent(value)); } - return fetch(API_SERVER_URL, { + return fetch(url, { ...init, headers: { "content-type": "application/json", diff --git a/apps/dashboard/src/hooks/useBuildId.ts b/apps/dashboard/src/hooks/useBuildId.ts deleted file mode 100644 index c9edb85b0e5..00000000000 --- a/apps/dashboard/src/hooks/useBuildId.ts +++ /dev/null @@ -1,38 +0,0 @@ -import posthog from "posthog-js"; -import { useCallback } from "react"; - -export const useBuildId = () => { - const shouldReload = useCallback((): boolean => { - if (process.env.NODE_ENV !== "production") { - return false; - } - - const nextData = document.querySelector("#__NEXT_DATA__"); - - const buildId = nextData?.textContent - ? JSON.parse(nextData.textContent).buildId - : null; - - const request = new XMLHttpRequest(); - request.open("HEAD", `/_next/static/${buildId}/_buildManifest.js`, false); - request.setRequestHeader("Pragma", "no-cache"); - request.setRequestHeader("Cache-Control", "no-cache"); - request.setRequestHeader( - "If-Modified-Since", - "Thu, 01 Jun 1970 00:00:00 GMT", - ); - request.send(null); - - if (request.status === 404) { - posthog.capture("should_reload", { - buildId, - }); - } - - return request.status === 404; - }, []); - - return { - shouldReload, - }; -}; diff --git a/apps/dashboard/src/page-id.ts b/apps/dashboard/src/page-id.ts deleted file mode 100644 index 2e271b7cf0f..00000000000 --- a/apps/dashboard/src/page-id.ts +++ /dev/null @@ -1,25 +0,0 @@ -// biome-ignore lint/nursery/noEnum: planned to be removed in the future -export enum PageId { - // none case (for previous page id) - None = "none", - - // --------------------------------------------------------------------------- - // marketing / growth pages - // --------------------------------------------------------------------------- - - // thirdweb.com/account-abstraction - SmartWalletLanding = "smart-wallet-landing", - - // thirdweb.com/embedded-wallets - EmbeddedWalletsLanding = "embedded-wallets-landing", - - // thirdweb.com/auth - AuthLanding = "auth-landing", - - // --------------------------------------------------------------------------- - // general product pages - // --------------------------------------------------------------------------- - - // thirdweb.com/404 - PageNotFound = "page-not-found", -} diff --git a/apps/dashboard/src/pages/404.tsx b/apps/dashboard/src/pages/404.tsx deleted file mode 100644 index bbd6f5e05e9..00000000000 --- a/apps/dashboard/src/pages/404.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PageId } from "page-id"; -import type { ThirdwebNextPage } from "utils/types"; -import { NotFoundPage } from "../components/not-found-page"; - -const PageNotFound: ThirdwebNextPage = () => { - return ; -}; - -PageNotFound.pageId = PageId.PageNotFound; - -export default PageNotFound; diff --git a/apps/dashboard/src/pages/DO_NOT_ADD_THINGS_HERE.md b/apps/dashboard/src/pages/DO_NOT_ADD_THINGS_HERE.md deleted file mode 100644 index d869a3b0f16..00000000000 --- a/apps/dashboard/src/pages/DO_NOT_ADD_THINGS_HERE.md +++ /dev/null @@ -1,3 +0,0 @@ -# DO NOT ADD NEW THINGS INSIDE THIS FOLDER OR SUB_FOLDERS - -This part of the dashboard is deprecated, and we are actively working on removing it. Please do not add new things here. If you need to add new things, please add them to the appropriate folder in the src folder. \ No newline at end of file diff --git a/apps/dashboard/src/pages/_app.tsx b/apps/dashboard/src/pages/_app.tsx deleted file mode 100644 index 14b148a2c2c..00000000000 --- a/apps/dashboard/src/pages/_app.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import { ThemeProvider } from "@/components/theme-provider"; -import { Toaster } from "@/components/ui/sonner"; -import { ChakraProvider, useColorMode } from "@chakra-ui/react"; -import { Global, css } from "@emotion/react"; -import type { DehydratedState } from "@tanstack/react-query"; -import { ProgressBar } from "components/shared/ProgressBar"; -import { useBuildId } from "hooks/useBuildId"; -import PlausibleProvider from "next-plausible"; -import { DefaultSeo } from "next-seo"; -import { useTheme } from "next-themes"; -import type { AppProps } from "next/app"; -import { - IBM_Plex_Mono as ibmPlexMonoConstructor, - Inter as interConstructor, -} from "next/font/google"; -import { useRouter } from "next/router"; -import { PageId } from "page-id"; -import posthog from "posthog-js"; -import { memo, useEffect, useMemo, useRef } from "react"; -import { generateBreakpointTypographyCssVars } from "tw-components/utils/typography"; -import type { ThirdwebNextPage } from "utils/types"; -import chakraTheme from "../theme"; -import "@/styles/globals.css"; -import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter"; -import { UnlimitedWalletsBanner } from "../components/notices/AnnouncementBanner"; - -const inter = interConstructor({ - subsets: ["latin"], - display: "swap", - fallback: ["system-ui", "Helvetica Neue", "Arial", "sans-serif"], - adjustFontFallback: true, -}); - -const ibmPlexMono = ibmPlexMonoConstructor({ - weight: ["400", "500", "600", "700"], - subsets: ["latin"], - display: "swap", - fallback: ["Consolas", "Courier New", "monospace"], -}); - -const chakraThemeWithFonts = { - ...chakraTheme, - fonts: { - ...chakraTheme.fonts, - heading: inter.style.fontFamily, - body: inter.style.fontFamily, - mono: ibmPlexMono.style.fontFamily, - }, -}; - -const fontSizeCssVars = generateBreakpointTypographyCssVars(); - -type AppPropsWithLayout = AppProps<{ dehydratedState?: DehydratedState }> & { - Component: ThirdwebNextPage; -}; - -const ConsoleAppWrapper: React.FC = ({ - Component, - pageProps, -}) => { - // run this ONCE on app load - // eslint-disable-next-line no-restricted-syntax - - const router = useRouter(); - const { shouldReload } = useBuildId(); - - // legit use-case, will go away as part of app router rewrite (once finished) - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - const handleRouteChange = async () => { - if (shouldReload()) { - router.reload(); - } - }; - - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, [router, shouldReload]); - - // legit use-case - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - // Taken from StackOverflow. Trying to detect both Safari desktop and mobile. - const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - if (isSafari) { - // This is kind of a lie. - // We still rely on the manual Next.js scrollRestoration logic. - // However, we *also* don't want Safari grey screen during the back swipe gesture. - // Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time. - history.scrollRestoration = "auto"; - } else { - // For other browsers, let Next.js set scrollRestoration to 'manual'. - // It seems to work better for Chrome and Firefox which don't animate the back swipe. - } - }, []); - - // legit use-case - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - // Init PostHog - posthog.init( - process.env.NEXT_PUBLIC_POSTHOG_API_KEY || - "phc_hKK4bo8cHZrKuAVXfXGpfNSLSJuucUnguAgt2j6dgSV", - { - api_host: "https://a.thirdweb.com", - autocapture: true, - debug: false, - capture_pageview: false, - disable_session_recording: true, - }, - ); - // register the git commit sha on all subsequent events - posthog.register({ - tw_dashboard_version: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, - }); - // defer session recording start by 2 seconds because it synchronously loads JS - const t = setTimeout(() => { - posthog.startSessionRecording(); - }, 2_000); - return () => { - clearTimeout(t); - }; - }, []); - - const pageId = - typeof Component.pageId === "function" - ? Component.pageId(pageProps) - : Component.pageId; - - // starts out with "none" page id - const prevPageId = useRef(PageId.None); - // legit use-case - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - // this catches the case where the hook is called twice on the same page - if (pageId === prevPageId.current) { - return; - } - posthog.register({ - page_id: pageId, - previous_page_id: prevPageId.current, - }); - posthog.capture("$pageview"); - return () => { - prevPageId.current = pageId; - }; - }, [pageId]); - - const canonicalUrl = useMemo(() => { - const base = "https://thirdweb.com"; - // replace all re-written middleware paths - const path = router.asPath - .replace("/evm/", "/") - .replace("/chain/", "/") - .replace("/publish/", "/"); - return `${base}${path}`; - }, [router.asPath]); - - return ( - - ); -}; - -interface ConsoleAppProps { - Component: AppPropsWithLayout["Component"]; - pageProps: AppPropsWithLayout["pageProps"]; - seoCanonical: string; - isFallback?: boolean; -} - -const ConsoleApp = memo(function ConsoleApp({ - Component, - pageProps, - seoCanonical, - isFallback, -}: ConsoleAppProps) { - const getLayout = Component.getLayout ?? ((page) => page); - - return ( - - div > svg { - font-size: 10px !important; - } - `} - /> - - - - - - - - - - {isFallback && Component.fallback - ? Component.fallback - : getLayout(, pageProps)} - - - - - - ); -}); - -function TailwindTheme(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} - -const SyncTheme: React.FC = () => { - const { theme, setTheme } = useTheme(); - const { setColorMode } = useColorMode(); - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - setColorMode(theme === "light" ? "light" : "dark"); - }, [setColorMode, theme]); - - // handle dashboard with now old "system" set - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if (theme === "system") { - setTheme("dark"); - setColorMode("dark"); - } - }, [theme, setTheme, setColorMode]); - - return null; -}; - -export default ConsoleAppWrapper; diff --git a/apps/dashboard/src/pages/_document.tsx b/apps/dashboard/src/pages/_document.tsx deleted file mode 100644 index 6ac3119dc26..00000000000 --- a/apps/dashboard/src/pages/_document.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ColorModeScript } from "@chakra-ui/react"; -import Document, { Head, Html, Main, NextScript } from "next/document"; -import chakraTheme from "../theme"; - -class ConsoleDocument extends Document { - render() { - return ( - - - {/* preconnect to domains we know we'll be using */} - - - - - - - -
- - - - ); - } -} - -export default ConsoleDocument; diff --git a/apps/dashboard/src/pages/_error.tsx b/apps/dashboard/src/pages/_error.tsx deleted file mode 100644 index 3dd9179de70..00000000000 --- a/apps/dashboard/src/pages/_error.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/** - * This page is loaded by Nextjs: - * - on the server, when data-fetching methods throw or reject - * - on the client, when `getInitialProps` throws or rejects - * - on the client, when a React lifecycle method throws or rejects, and it's - * caught by the built-in Nextjs error boundary - * - * See: - * - https://nextjs.org/docs/basic-features/data-fetching/overview - * - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props - * - https://reactjs.org/docs/error-boundaries.html - */ -import * as Sentry from "@sentry/nextjs"; -import type { NextPage } from "next"; -import NextErrorComponent, { type ErrorProps } from "next/error"; - -const CustomErrorComponent: NextPage = (props) => ( - -); - -CustomErrorComponent.getInitialProps = async (contextData) => { - // In case this is running in a serverless function, await this in order to give Sentry - // time to send the error before the lambda exits - await Sentry.captureUnderscoreErrorException(contextData); - - if (contextData.err instanceof Error) { - Sentry.withScope((scope) => { - scope.setTag("page-crashed", "true"); - scope.setLevel("fatal"); - Sentry.captureException(contextData.err, { - extra: { - crashedPage: true, - boundary: "global", - router: "pages", - }, - }); - }); - } - - // This will contain the status code of the response - return NextErrorComponent.getInitialProps(contextData); -}; - -export default CustomErrorComponent; diff --git a/apps/dashboard/src/pages/account-abstraction.tsx b/apps/dashboard/src/pages/account-abstraction.tsx deleted file mode 100644 index a604eab7685..00000000000 --- a/apps/dashboard/src/pages/account-abstraction.tsx +++ /dev/null @@ -1,202 +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 { LandingLayout } from "components/landing-pages/layout"; -import { LandingShowcaseImage } from "components/landing-pages/showcase-image"; -import { getAbsoluteUrl } from "lib/vercel-utils"; -import { PageId } from "page-id"; -import { Heading, Text } from "tw-components"; -import type { ThirdwebNextPage } from "utils/types"; - -const TRACKING_CATEGORY = "smart-wallet-landing"; - -const GUIDES = [ - { - title: "The Quick-Start Guide to Account Abstraction", - image: require("../../public/assets/product-pages/smart-wallet/get-started.png"), - link: "https://portal.thirdweb.com/wallets/smart-wallet/get-started", - }, - { - title: "Choosing Between Simple, Managed, & Dynamic Smart Accounts", - image: require("../../public/assets/product-pages/smart-wallet/which-contract.png"), - 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: require("../../public/assets/product-pages/smart-wallet/batch-txns.png"), - link: "https://blog.thirdweb.com/guides/how-to-batch-transactions-with-the-thirdweb-sdk/", - }, -]; - -const SmartWallet: ThirdwebNextPage = () => { - return ( - - - - - ), - }, - { - 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: ( - - ), - }, - { - title: "Enterprise-grade security", - description: - "Wallet recovery, 2FA, and multi-signature support for ultimate peace of mind — for users & teams.", - Component: ( - - ), - }, - ]} - /> - - - An all-in-one solution for - - Account Abstraction - - - - Implement account abstraction into any web3 app — with a - best-in-class SDK, full wallet customizability, and managed - infrastructure. - - - - - - - - - - - - - ); -}; - -SmartWallet.pageId = PageId.SmartWalletLanding; - -export default SmartWallet; diff --git a/apps/dashboard/src/pages/api/apply-op-sponsorship.ts b/apps/dashboard/src/pages/api/apply-op-sponsorship.ts deleted file mode 100644 index c0b52efadba..00000000000 --- a/apps/dashboard/src/pages/api/apply-op-sponsorship.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import invariant from "tiny-invariant"; - -export const config = { - runtime: "edge", -}; - -interface ApplyOpSponsorshipPayload { - // biome-ignore lint/suspicious/noExplicitAny: FIXME - fields: any; -} - -const handler = async (req: NextRequest) => { - if (req.method !== "POST") { - return NextResponse.json({ error: "invalid method" }, { status: 400 }); - } - - const requestBody = (await req.json()) as ApplyOpSponsorshipPayload; - - const { fields } = requestBody; - invariant(process.env.HUBSPOT_ACCESS_TOKEN, "missing HUBSPOT_ACCESS_TOKEN"); - - const response = await fetch( - "https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/2fbf6a3b-d4cc-4a23-a4f5-42674e8487b9", - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`, - }, - method: "POST", - body: JSON.stringify({ fields }), - }, - ); - - if (!response.ok) { - const body = await response.json(); - console.error("error", body); - } - - return NextResponse.json( - { status: response.statusText }, - { - status: response.status, - }, - ); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/api/email-signup.ts b/apps/dashboard/src/pages/api/email-signup.ts deleted file mode 100644 index 75ff46f5471..00000000000 --- a/apps/dashboard/src/pages/api/email-signup.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import invariant from "tiny-invariant"; - -export const config = { - runtime: "edge", -}; - -interface EmailSignupPayload { - email: string; - send_welcome_email?: boolean; -} - -const handler = async (req: NextRequest) => { - if (req.method !== "POST") { - return NextResponse.json({ error: "invalid method" }, { status: 400 }); - } - - const requestBody = (await req.json()) as EmailSignupPayload; - - const { email, send_welcome_email = false } = requestBody; - invariant(process.env.BEEHIIV_API_KEY, "missing BEEHIIV_API_KEY"); - - const response = await fetch( - "https://api.beehiiv.com/v2/publications/pub_9f54090a-6d14-406b-adfd-dbb30574f664/subscriptions", - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.BEEHIIV_API_KEY}`, - }, - method: "POST", - body: JSON.stringify({ - email, - send_welcome_email, - utm_source: "thirdweb.com", - }), - }, - ); - - return NextResponse.json( - { status: response.statusText }, - { - status: response.status, - }, - ); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/api/server-proxy/analytics/[...paths].ts b/apps/dashboard/src/pages/api/server-proxy/analytics/[...paths].ts deleted file mode 100644 index 8e9bac6c576..00000000000 --- a/apps/dashboard/src/pages/api/server-proxy/analytics/[...paths].ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { NextRequest } from "next/server"; - -export const config = { - runtime: "edge", -}; -const handler = async (req: NextRequest) => { - // get the full path from the request including query params - let pathname = req.nextUrl.pathname; - - // remove the /api/server-proxy prefix - pathname = pathname.replace(/^\/api\/server-proxy\/analytics/, ""); - - const searchParams = req.nextUrl.searchParams; - searchParams.delete("paths"); - - // create a new URL object for the analytics server - const API_SERVER_URL = new URL( - process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com", - ); - API_SERVER_URL.pathname = pathname; - searchParams.forEach((value, key) => { - API_SERVER_URL.searchParams.append(key, value); - }); - - return fetch(API_SERVER_URL, { - method: req.method, - headers: { - "content-type": "application/json", - authorization: `Bearer ${process.env.ANALYTICS_SERVICE_API_KEY}`, - }, - body: req.body, - }); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/api/server-proxy/api/[...paths].tsx b/apps/dashboard/src/pages/api/server-proxy/api/[...paths].tsx deleted file mode 100644 index df4c31e96e7..00000000000 --- a/apps/dashboard/src/pages/api/server-proxy/api/[...paths].tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; -import { API_SERVER_URL } from "@/constants/env"; -import type { NextRequest } from "next/server"; -import { getAddress } from "thirdweb"; - -export const config = { - runtime: "edge", -}; -const handler = async (req: NextRequest) => { - const activeAccount = req.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value; - const authToken = activeAccount - ? req.cookies.get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value - : null; - - // get the full path from the request including query params - let pathname = req.nextUrl.pathname; - - // remove the /api/server-proxy prefix - pathname = pathname.replace(/^\/api\/server-proxy\/api/, ""); - - const searchParams = req.nextUrl.searchParams; - searchParams.delete("paths"); - - // create a new URL object for the API server - const url = new URL(API_SERVER_URL); - url.pathname = pathname; - searchParams.forEach((value, key) => { - url.searchParams.append(key, value); - }); - - return fetch(url, { - method: req.method, - headers: { - "content-type": "application/json", - // pass the auth token if we have one - ...(authToken ? { authorization: `Bearer ${authToken}` } : {}), - }, - body: req.body, - }); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/api/server-proxy/pay/[...paths].tsx b/apps/dashboard/src/pages/api/server-proxy/pay/[...paths].tsx deleted file mode 100644 index 2f6d88fa828..00000000000 --- a/apps/dashboard/src/pages/api/server-proxy/pay/[...paths].tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; -import type { NextRequest } from "next/server"; -import { getAddress } from "thirdweb"; - -export const config = { - runtime: "edge", -}; -const handler = async (req: NextRequest) => { - const activeAccount = req.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value; - const authToken = activeAccount - ? req.cookies.get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value - : null; - - // get the full path from the request including query params - let pathname = req.nextUrl.pathname; - - // remove the /api/server-proxy prefix - pathname = pathname.replace(/^\/api\/server-proxy\/pay/, ""); - - const searchParams = req.nextUrl.searchParams; - searchParams.delete("paths"); - - // create a new URL object for the API server - const PAY_SERVER_URL = new URL( - process.env.NEXT_PUBLIC_PAY_URL - ? `https://${process.env.NEXT_PUBLIC_PAY_URL}` - : "https://pay.thirdweb-dev.com", - ); - PAY_SERVER_URL.pathname = pathname; - searchParams.forEach((value, key) => { - PAY_SERVER_URL.searchParams.append(key, value); - }); - - return fetch(PAY_SERVER_URL, { - method: req.method, - headers: { - "content-type": "application/json", - // pass the auth token if we have one - ...(authToken ? { authorization: `Bearer ${authToken}` } : {}), - }, - body: req.body, - }); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/api/wallet/nfts/[chainId].ts b/apps/dashboard/src/pages/api/wallet/nfts/[chainId].ts deleted file mode 100644 index efcac10efce..00000000000 --- a/apps/dashboard/src/pages/api/wallet/nfts/[chainId].ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - generateAlchemyUrl, - isAlchemySupported, - transformAlchemyResponseToNFT, -} from "lib/wallet/nfts/alchemy"; -import { - generateMoralisUrl, - isMoralisSupported, - transformMoralisResponseToNFT, -} from "lib/wallet/nfts/moralis"; -import { - generateSimpleHashUrl, - isSimpleHashSupported, - transformSimpleHashResponseToNFT, -} from "lib/wallet/nfts/simpleHash"; -import type { WalletNFT } from "lib/wallet/nfts/types"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { getSingleQueryValue } from "utils/router"; - -export type WalletNFTApiReturn = - | { result: WalletNFT[]; error?: never } - | { result?: never; error: string }; - -const handler = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - if (req.method !== "GET") { - return res.status(400).json({ error: "invalid method" }); - } - const queryChainId = getSingleQueryValue(req.query, "chainId"); - const owner = getSingleQueryValue(req.query, "owner"); - if (!queryChainId) { - return res.status(400).json({ error: "missing chainId" }); - } - if (!owner) { - return res.status(400).json({ error: "missing owner" }); - } - const chainId = Number.parseInt(queryChainId); - - const supportedChainSlug = await isSimpleHashSupported(chainId); - - if (supportedChainSlug && process.env.SIMPLEHASH_API_KEY) { - const url = generateSimpleHashUrl({ chainSlug: supportedChainSlug, owner }); - - const options = { - method: "GET", - headers: { - "X-API-KEY": process.env.SIMPLEHASH_API_KEY, - }, - }; - const response = await fetch(url, options); - - if (response.status >= 400) { - return res.status(response.status).json({ error: response.statusText }); - } - try { - const parsedResponse = await response.json(); - const result = await transformSimpleHashResponseToNFT( - parsedResponse, - owner, - ); - - res.setHeader( - "Cache-Control", - "public, s-maxage=10, stale-while-revalidate=59", - ); - return res.status(200).json({ result }); - } catch (err) { - console.error("Error fetching NFTs", err); - return res.status(500).json({ error: "error parsing response" }); - } - } - - if (isAlchemySupported(chainId)) { - const url = generateAlchemyUrl({ chainId, owner }); - - const response = await fetch(url); - if (response.status >= 400) { - return res.status(response.status).json({ error: response.statusText }); - } - try { - const parsedResponse = await response.json(); - const result = await transformAlchemyResponseToNFT(parsedResponse, owner); - - res.setHeader( - "Cache-Control", - "public, s-maxage=10, stale-while-revalidate=59", - ); - return res.status(200).json({ result }); - } catch (err) { - console.error("Error fetching NFTs", err); - return res.status(500).json({ error: "error parsing response" }); - } - } - - if (isMoralisSupported(chainId) && process.env.MORALIS_API_KEY) { - const url = generateMoralisUrl({ chainId, owner }); - - const options = { - method: "GET", - headers: { - "X-API-Key": process.env.MORALIS_API_KEY, - }, - }; - const response = await fetch(url, options); - - if (response.status >= 400) { - return res.status(response.status).json({ error: response.statusText }); - } - - try { - const parsedResponse = await response.json(); - const result = await transformMoralisResponseToNFT( - await parsedResponse, - owner, - ); - - res.setHeader( - "Cache-Control", - "public, s-maxage=10, stale-while-revalidate=59", - ); - return res.status(200).json({ result }); - } catch (err) { - console.error("Error fetching NFTs", err); - return res.status(500).json({ error: "error parsing response" }); - } - } - - return res.status(400).json({ error: "unsupported chain" }); -}; - -export default handler; diff --git a/apps/dashboard/src/pages/auth.tsx b/apps/dashboard/src/pages/auth.tsx deleted file mode 100644 index 36bd15a38a7..00000000000 --- a/apps/dashboard/src/pages/auth.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Container, Flex } from "@chakra-ui/react"; -import { LandingEndCTA } from "components/landing-pages/end-cta"; -import { LandingGridSection } from "components/landing-pages/grid-section"; -import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase"; -import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image"; -import { LandingIconSectionItem } from "components/landing-pages/icon-section-item"; -import { LandingLayout } from "components/landing-pages/layout"; -import { LandingSectionHeading } from "components/landing-pages/section-heading"; -import { getAbsoluteUrl } from "lib/vercel-utils"; -import { PageId } from "page-id"; -import { Card, TrackedLink } from "tw-components"; -import type { ThirdwebNextPage } from "utils/types"; - -const TRACKING_CATEGORY = "auth-landing"; - -const GUIDES = [ - { - title: "How to Build a Web3 Creator Platform with a Web2 Backend", - image: - "https://blog.thirdweb.com/content/images/size/w2000/2023/03/How-to-create-a-web3-creator----platform-with-a-web2-backend.png", - link: "https://blog.thirdweb.com/guides/how-to-create-a-web3-creator-platform/", - }, - { - title: "Create An NFT Gated Website", - image: - "https://blog.thirdweb.com/content/images/size/w2000/2022/08/thumbnail-31.png", - link: "https://blog.thirdweb.com/guides/nft-gated-website/", - }, - { - title: "Accept Stripe Subscription Payments For Your Web3 App", - image: - "https://blog.thirdweb.com/content/images/size/w2000/2023/03/Add-stripe-subscriptions--with-web3-auth-2.png", - link: "https://blog.thirdweb.com/guides/add-stripe-subscriptions-with-web3-auth/", - }, -]; - -const AuthLanding: ThirdwebNextPage = () => { - return ( - - - - - - - - - - - - - Built on the SIWE ( - - Sign-in with Ethereum - - ) standard. Securely verify a user's on-chain identity, - without relying on a centralized database to verify their - identity. - - } - /> - - - - - - - Secure your backend with a web3-compatible authentication - system compliant with the widely used{" "} - - JSON Web Token - {" "} - standard. - - } - /> - - - - - } - > - - - - - - - - - - - - - - - - - ); -}; - -AuthLanding.pageId = PageId.AuthLanding; - -export default AuthLanding; diff --git a/apps/dashboard/src/pages/in-app-wallets.tsx b/apps/dashboard/src/pages/in-app-wallets.tsx deleted file mode 100644 index ef08de0cca4..00000000000 --- a/apps/dashboard/src/pages/in-app-wallets.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { Container, Flex } from "@chakra-ui/react"; -import { ChakraNextImage } from "components/Image"; -import { LandingCardWithImage } from "components/landing-pages/card-with-image"; -import { LandingDynamicSelector } from "components/landing-pages/dynamic-selector"; -import { LandingEndCTA } from "components/landing-pages/end-cta"; -import { LandingGridSection } from "components/landing-pages/grid-section"; -import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase"; -import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image"; -import { LandingLayout } from "components/landing-pages/layout"; -import { getAbsoluteUrl } from "lib/vercel-utils"; -import { PageId } from "page-id"; -import { Heading } from "tw-components"; -import type { ThirdwebNextPage } from "utils/types"; - -const TRACKING_CATEGORY = "embedded-wallets-landing"; - -const GUIDES = [ - { - title: "Docs: In-App Wallets Overview", - image: require("../../public/assets/product-pages/embedded-wallets/embedded-wallet.png"), - link: "https://portal.thirdweb.com/connect/in-app-wallet/overview", - }, - { - title: "Live Demo: In-App Wallets", - image: require("../../public/assets/product-pages/embedded-wallets/paper.png"), - link: "https://catattack.thirdweb.com", - }, - { - title: "Quick-Start Template: In-App Wallet + Account Abstraction", - image: require("../../public/assets/product-pages/smart-wallet/get-started.png"), - link: "https://github.com/thirdweb-example/embedded-smart-wallet", - }, -]; - -const EmbeddedWalletsLanding: ThirdwebNextPage = () => { - return ( - - - - - - ), - }, - { - title: "Integrate with your own custom auth", - description: - "Spin up in-app wallets for your users with your app or game's existing auth system.", - Component: ( - - ), - }, - { - title: "Cross-platform support", - description: - "Enable users to log into their accounts (and access their wallets) from any device, in one click. Support for web, mobile, & Unity.", - Component: ( - - ), - }, - ]} - /> - - - - Abstract away complexity for your users - -
- } - > - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -EmbeddedWalletsLanding.pageId = PageId.EmbeddedWalletsLanding; - -export default EmbeddedWalletsLanding; diff --git a/apps/dashboard/src/theme/index.ts b/apps/dashboard/src/theme/index.ts index 9347f26f23f..95803f8a4fe 100644 --- a/apps/dashboard/src/theme/index.ts +++ b/apps/dashboard/src/theme/index.ts @@ -1,3 +1,5 @@ +"use client"; + import { type Theme, extendTheme } from "@chakra-ui/react"; import { getColor, mode } from "@chakra-ui/theme-tools"; import { skeletonTheme } from "./chakra-componens/skeleton"; diff --git a/apps/dashboard/src/tw-components/button.tsx b/apps/dashboard/src/tw-components/button.tsx index d32d3feaa86..345f163e70a 100644 --- a/apps/dashboard/src/tw-components/button.tsx +++ b/apps/dashboard/src/tw-components/button.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Button as ChakraButton, type ButtonProps as ChakraButtonProps, diff --git a/apps/dashboard/src/utils/router.ts b/apps/dashboard/src/utils/router.ts deleted file mode 100644 index c863c80c1a0..00000000000 --- a/apps/dashboard/src/utils/router.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ParsedUrlQuery } from "node:querystring"; - -/** - * @deprecated use `SearchParams` instead - */ -export function getSingleQueryValue( - query: ParsedUrlQuery, - key: string, -): string | undefined { - const _val = query[key]; - - return Array.isArray(_val) ? _val[0] : _val; -} diff --git a/apps/dashboard/src/utils/types.ts b/apps/dashboard/src/utils/types.ts deleted file mode 100644 index 0de2b28e85c..00000000000 --- a/apps/dashboard/src/utils/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { NextPage } from "next"; -import type { PageId } from "page-id"; -import type { ReactElement, ReactNode } from "react"; - -// biome-ignore lint/suspicious/noExplicitAny: FIXME -export type ThirdwebNextPage = NextPage & { - // biome-ignore lint/suspicious/noExplicitAny: FIXME - getLayout?: (page: ReactElement, pageProps?: any) => ReactNode; - // biome-ignore lint/suspicious/noExplicitAny: FIXME - pageId: PageId | ((pageProps: any) => PageId); - fallback?: React.ReactNode; -}; - -/** - * Makes a parameter required to be passed, but still allows it to be null or undefined. - * @internal - */ -export type RequiredParam = T | null | undefined; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06ca6ea8e62..b6bdee49e3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,9 +205,6 @@ importers: next-plausible: specifier: ^3.12.4 version: 3.12.4(next@15.1.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - next-seo: - specifier: ^6.5.0 - version: 6.6.0(next@15.1.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes: specifier: ^0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -10500,13 +10497,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - next-seo@6.6.0: - resolution: {integrity: sha512-0VSted/W6XNtgAtH3D+BZrMLLudqfm0D5DYNJRXHcDgan/1ZF1tDFIsWrmvQlYngALyphPfZ3ZdOqlKpKdvG6w==} - peerDependencies: - next: ^8.1.1-canary.54 || >=9.0.0 - react: '>=16.0.0' - react-dom: '>=16.0.0' - next-sitemap@4.2.3: resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} engines: {node: '>=14.18'} @@ -26957,12 +26947,6 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - next-seo@6.6.0(next@15.1.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): - dependencies: - next: 15.1.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - next-sitemap@4.2.3(next@15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): dependencies: '@corex/deepmerge': 4.0.43