api proxy (#3603)
<!-- start pr-codex -->
## PR-Codex overview
This PR focuses on enhancing JWT utils, updating API host paths, and optimizing user authentication handling.
### Detailed summary
- Export JWT utils, fix naming of `decodeJWT`
- Update API host paths to use relative paths
- Optimize user authentication handling in various components
> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-delete-partner.ts`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/components/buttons/TransactionButton.tsx`, `apps/dashboard/src/pages/dashboard/settings/notifications.tsx`, `apps/dashboard/src/components/engine/overview/transaction-timeline.tsx`, `apps/dashboard/src/pages/dashboard/settings/devices.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-add-partner.ts`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-ecosystem.ts`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/pay/page.tsx`, `apps/dashboard/src/pages/dashboard/engine/index.tsx`, `apps/dashboard/src/pages/dashboard/settings/billing.tsx`, `apps/dashboard/src/components/engine/engine-instances-table.tsx`, `apps/dashboard/src/pages/dashboard/settings/usage.tsx`, `apps/dashboard/src/pages/dashboard/settings/gas-credits.tsx`, `apps/dashboard/src/pages/dashboard/settings/api-keys/index.tsx`, `apps/dashboard/src/pages/api/server-proxy/api/[...paths].tsx`, `apps/dashboard/src/pages/dashboard/connect/analytics.tsx`, `apps/dashboard/src/components/storage/your-files.tsx`, `apps/dashboard/src/pages/api/server-proxy/pay/[...paths].tsx`, `apps/dashboard/src/components/engine/engine-list.tsx`, `apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayVolume.ts`, `apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayNewCustomers.ts`, `apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayPurchases.ts`, `apps/dashboard/src/pages/dashboard/connect/in-app-wallets.tsx`, `apps/dashboard/src/middleware.ts`, `apps/dashboard/src/components/pay/PayAnalytics/hooks/usePayCustomers.ts`, `apps/dashboard/src/pages/dashboard/connect/account-abstraction.tsx`, `apps/dashboard/src/app/login/page.tsx`, `apps/dashboard/src/app/components/ThirdwebMiniLogo.tsx`, `apps/dashboard/src/app/api/auth/ensure-login/route.ts`, `apps/dashboard/src/app/login/auth-actions.ts`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useLoggedInUser.ts`, `apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts`
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
export JWT utils, fix naming of `decodeJWT`
|
||||
@@ -0,0 +1,7 @@
|
||||
export const LOGGED_IN_ONLY_PATHS = [
|
||||
// anything that _starts_ with /dashboard is logged in only
|
||||
"/dashboard",
|
||||
// anything that _starts_ with /cli is logged in only
|
||||
"/cli",
|
||||
// TODO: add any other logged in only paths here
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export const COOKIE_ACTIVE_ACCOUNT = "tw_active_account";
|
||||
export const COOKIE_PREFIX_TOKEN = "tw_token_";
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSupportedChains } from "@thirdweb-dev/react";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import {
|
||||
useAddRecentlyUsedChainId,
|
||||
@@ -14,89 +12,12 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { ConnectButton, type SiweAuthOptions } from "thirdweb/react";
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
|
||||
import { fetchUserQuery } from "../../hooks/useLoggedInUser";
|
||||
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
|
||||
import { popularChains } from "../popularChains";
|
||||
import { clearAccessToken, setAccessToken } from "./useAccessToken";
|
||||
|
||||
// TODO remove all of this
|
||||
|
||||
type FetchAuthTokenResponse = {
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
const TOKEN_PROMISE_MAP = new Map<string, Promise<FetchAuthTokenResponse>>();
|
||||
|
||||
async function fetchAuthToken(
|
||||
address: string,
|
||||
abortController?: AbortController,
|
||||
): Promise<FetchAuthTokenResponse> {
|
||||
if (!address) {
|
||||
throw new Error("address is required");
|
||||
}
|
||||
if (TOKEN_PROMISE_MAP.has(address)) {
|
||||
return TOKEN_PROMISE_MAP.get(address) as Promise<FetchAuthTokenResponse>;
|
||||
}
|
||||
const promise = new Promise<FetchAuthTokenResponse>((resolve, reject) => {
|
||||
return fetch(`${THIRDWEB_API_HOST}/v1/auth/token`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: abortController?.signal,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return {
|
||||
jwt: json.data.jwt as string,
|
||||
};
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => {
|
||||
// remove the promise from the map when it's done
|
||||
TOKEN_PROMISE_MAP.delete(address);
|
||||
});
|
||||
});
|
||||
TOKEN_PROMISE_MAP.set(address, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// END TODO
|
||||
|
||||
export async function logout() {
|
||||
// reset the token ASAP
|
||||
clearAccessToken();
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/logout`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to logout");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface ConnectWalletProps {
|
||||
shrinkMobile?: boolean;
|
||||
upsellTestnet?: boolean;
|
||||
onChainSelect?: (chainId: number) => void;
|
||||
noAuth?: boolean;
|
||||
disableChainConfig?: boolean;
|
||||
disableAddCustomNetwork?: boolean;
|
||||
}
|
||||
|
||||
export const CustomConnectWallet: React.FC<ConnectWalletProps> = ({
|
||||
noAuth,
|
||||
}) => {
|
||||
export const CustomConnectWallet: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const recentChainsv4 = useRecentlyUsedChains();
|
||||
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
|
||||
@@ -126,78 +47,11 @@ export const CustomConnectWallet: React.FC<ConnectWalletProps> = ({
|
||||
];
|
||||
}, [recentChainsv4, favChainsQuery.data]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const authConfig = useMemo(() => {
|
||||
if (noAuth) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
getLoginPayload: async (params) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/payload`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
address: params.address,
|
||||
chainId: params.chainId.toString(),
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch login payload");
|
||||
}
|
||||
return (await res.json()).payload;
|
||||
},
|
||||
doLogin: async (payload) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ payload }),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to login");
|
||||
}
|
||||
const json = await res.json();
|
||||
|
||||
if (json.token) {
|
||||
setAccessToken(json.token);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["logged_in_user"],
|
||||
});
|
||||
}
|
||||
return json;
|
||||
},
|
||||
doLogout: async () => {
|
||||
const l = await logout();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["logged_in_user"],
|
||||
});
|
||||
return l;
|
||||
},
|
||||
isLoggedIn: async (address) => {
|
||||
const user = await queryClient.fetchQuery(fetchUserQuery(address));
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { jwt } = await fetchAuthToken(address);
|
||||
if (jwt) {
|
||||
setAccessToken(jwt);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
} satisfies SiweAuthOptions;
|
||||
}, [noAuth, queryClient]);
|
||||
// ensures login status on pages that need it
|
||||
useLoggedInUser();
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
auth={authConfig}
|
||||
theme={t}
|
||||
client={thirdwebClient}
|
||||
connectModal={{
|
||||
@@ -211,6 +65,16 @@ export const CustomConnectWallet: React.FC<ConnectWalletProps> = ({
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
onDisconnect={async () => {
|
||||
try {
|
||||
// log out the user
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to log out", err);
|
||||
}
|
||||
}}
|
||||
chains={allChains}
|
||||
detailsModal={{
|
||||
networkSelector: {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
// historical reasons
|
||||
const GLOBAL_AUTH_TOKEN_KEY = "TW_AUTH_TOKEN";
|
||||
|
||||
let currentAccessToken: string | null = null;
|
||||
let listeners: Array<() => void> = [];
|
||||
|
||||
const accessTokenStore = {
|
||||
set: (value: string | null) => {
|
||||
// also set it on window (because of legacy things reading from there)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: needed
|
||||
(window as any)[GLOBAL_AUTH_TOKEN_KEY] = value;
|
||||
currentAccessToken = value;
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
},
|
||||
getSnapshot: () => currentAccessToken,
|
||||
subscribe: (listener: () => void) => {
|
||||
listeners = [...listeners, listener];
|
||||
return () => {
|
||||
listeners = listeners.filter((l) => l !== listener);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function setAccessToken(value: string) {
|
||||
accessTokenStore.set(value);
|
||||
}
|
||||
export function clearAccessToken() {
|
||||
accessTokenStore.set(null);
|
||||
}
|
||||
|
||||
export function useAccessToken() {
|
||||
return useSyncExternalStore(
|
||||
accessTokenStore.subscribe,
|
||||
accessTokenStore.getSnapshot,
|
||||
// TODO: we should actually be able to grab this server side once we move auth to the next server
|
||||
() => null,
|
||||
);
|
||||
}
|
||||
@@ -276,7 +276,6 @@ export function useAccount({ refetchInterval }: UseAccountInput = {}) {
|
||||
async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -304,7 +303,6 @@ export function useAccountUsage() {
|
||||
async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/usage`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -329,7 +327,7 @@ export function useAccountCredits() {
|
||||
async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/credits`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -363,7 +361,7 @@ export function useWalletStats(clientId: string | undefined) {
|
||||
`${THIRDWEB_API_HOST}/v1/account/wallets?clientId=${clientId}`,
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -391,7 +389,7 @@ export function useUpdateAccount() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -426,7 +424,7 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -470,7 +468,7 @@ export function useUpdateNotifications() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -506,7 +504,7 @@ export function useCreateBillingSession(enabled = false) {
|
||||
`${THIRDWEB_API_HOST}/v1/account/billingSession`,
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -534,7 +532,7 @@ export function useConfirmEmail() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -576,7 +574,7 @@ export function useConfirmEmbeddedWallet() {
|
||||
`${THIRDWEB_API_HOST}/v1/account/confirmEmbeddedWallet`,
|
||||
{
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -619,7 +617,7 @@ export function useResendEmailConfirmation() {
|
||||
`${THIRDWEB_API_HOST}/v1/account/resendEmailConfirmation`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -654,7 +652,7 @@ export function useCreatePaymentMethod() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/paymentMethod`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -687,7 +685,7 @@ export function useApiKeys() {
|
||||
async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -713,7 +711,7 @@ export function useCreateApiKey() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -747,7 +745,7 @@ export function useUpdateApiKey() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -782,7 +780,7 @@ export function useRevokeApiKey() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -814,7 +812,7 @@ export const usePolicies = (serviceId?: string) => {
|
||||
`${THIRDWEB_API_HOST}/v1/policies?serviceId=${serviceId}`,
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -836,7 +834,7 @@ export const useUpdatePolicies = () => {
|
||||
async (input: { serviceId: string; data: ApiKeyServicePolicy }) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -868,7 +866,7 @@ export function useAuthorizeWalletWithAccount() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/jwt/authorize-wallet`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${variables.token}`,
|
||||
@@ -902,7 +900,7 @@ export function useRevokeAuthorizedWallet() {
|
||||
`${THIRDWEB_API_HOST}/v1/authorized-wallets/${authorizedWalletId}/revoke`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -935,7 +933,7 @@ export function useAuthorizedWallets() {
|
||||
async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/authorized-wallets`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -960,8 +958,6 @@ export async function fetchChainsFromApi() {
|
||||
// TODO: re-visit this
|
||||
const res = await fetch("https://api.thirdweb.com/v1/chains", {
|
||||
method: "GET",
|
||||
// do not inclue credentials for chains endpoint
|
||||
// credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -107,7 +107,6 @@ export function useCreateTicket() {
|
||||
`${THIRDWEB_API_HOST}/v1/account/create-unthread-ticket`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,9 +35,9 @@ export function useEmbeddedWallets(clientId: string) {
|
||||
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
import { useAccessToken } from "../components/connect-wallet/useAccessToken";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
@@ -60,7 +59,6 @@ export function useEngineInstances() {
|
||||
async (): Promise<EngineInstance[]> => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
@@ -99,7 +97,7 @@ export type BackendWallet = {
|
||||
};
|
||||
|
||||
export function useEngineBackendWallets(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
[engineKeys.backendWallets(instance)],
|
||||
@@ -144,7 +142,6 @@ export function useEngineLatestVersion() {
|
||||
return useQuery(engineKeys.latestVersion(), async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/latest-version`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
@@ -164,7 +161,7 @@ export function useEngineUpdateVersion() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/update-version`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -190,7 +187,6 @@ export function useEngineRemoveFromDashboard() {
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
@@ -224,7 +220,7 @@ export function useEngineRemoveCloudHosted() {
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${instanceId}/remove-cloud-hosted`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -261,7 +257,7 @@ export function useEngineEditInstance() {
|
||||
async ({ instanceId, name, url }: EditEngineInstanceInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -342,7 +338,7 @@ type TransactionResponse = {
|
||||
* @returns
|
||||
*/
|
||||
export function useEngineTransactions(instance: string, autoUpdate: boolean) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.transactions(instance),
|
||||
@@ -383,7 +379,7 @@ type WalletConfig =
|
||||
};
|
||||
|
||||
export function useEngineWalletConfig(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.walletConfig(instance),
|
||||
@@ -413,7 +409,7 @@ export function useEngineBackendWalletBalance(
|
||||
instance: string,
|
||||
address: string,
|
||||
) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
|
||||
invariant(chainId, "chainId is required");
|
||||
@@ -444,7 +440,7 @@ export type EngineAdmin = {
|
||||
};
|
||||
|
||||
export function useEnginePermissions(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
return useQuery(
|
||||
@@ -479,7 +475,7 @@ export type AccessToken = {
|
||||
};
|
||||
|
||||
export function useEngineAccessTokens(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.accessTokens(instance),
|
||||
@@ -509,7 +505,7 @@ export type Keypair = {
|
||||
};
|
||||
|
||||
export function useEngineKeypairs(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.keypairs(instance),
|
||||
@@ -534,7 +530,7 @@ type AddKeypairInput = {
|
||||
};
|
||||
|
||||
export function useEngineAddKeypair(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -567,7 +563,7 @@ type RemoveKeypairInput = {
|
||||
};
|
||||
|
||||
export function useEngineRemoveKeypair(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -605,7 +601,7 @@ export type EngineRelayer = {
|
||||
};
|
||||
|
||||
export function useEngineRelayer(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.relayers(instance),
|
||||
@@ -632,7 +628,7 @@ export type CreateRelayerInput = {
|
||||
};
|
||||
|
||||
export function useEngineCreateRelayer(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -665,7 +661,7 @@ type RevokeRelayerInput = {
|
||||
};
|
||||
|
||||
export function useEngineRevokeRelayer(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -703,7 +699,7 @@ export type UpdateRelayerInput = {
|
||||
};
|
||||
|
||||
export function useEngineUpdateRelayer(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -742,7 +738,7 @@ export interface EngineWebhook {
|
||||
}
|
||||
|
||||
export function useEngineWebhooks(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.webhooks(instance),
|
||||
@@ -781,7 +777,7 @@ export type SetWalletConfigInput =
|
||||
};
|
||||
|
||||
export function useEngineSetWalletConfig(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -814,7 +810,7 @@ export type CreateBackendWalletInput = {
|
||||
};
|
||||
|
||||
export function useEngineCreateBackendWallet(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -850,7 +846,7 @@ interface UpdateBackendWalletInput {
|
||||
}
|
||||
|
||||
export function useEngineUpdateBackendWallet(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -901,7 +897,7 @@ export type ImportBackendWalletInput =
|
||||
};
|
||||
|
||||
export function useEngineImportBackendWallet(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -932,7 +928,7 @@ export function useEngineImportBackendWallet(instance: string) {
|
||||
}
|
||||
|
||||
export function useEngineGrantPermissions(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -965,7 +961,7 @@ type RevokePermissionsInput = {
|
||||
};
|
||||
|
||||
export function useEngineRevokePermissions(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -998,7 +994,7 @@ type CreateAccessTokenResponse = AccessToken & {
|
||||
};
|
||||
|
||||
export function useEngineCreateAccessToken(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -1031,7 +1027,7 @@ type RevokeAccessTokenInput = {
|
||||
};
|
||||
|
||||
export function useEngineRevokeAccessToken(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -1065,7 +1061,7 @@ type UpdateAccessTokenInput = {
|
||||
};
|
||||
|
||||
export function useEngineUpdateAccessToken(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -1100,7 +1096,7 @@ export type CreateWebhookInput = {
|
||||
};
|
||||
|
||||
export function useEngineCreateWebhook(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -1133,7 +1129,7 @@ type RevokeWebhookInput = {
|
||||
};
|
||||
|
||||
export function useEngineRevokeWebhook(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
@@ -1170,7 +1166,7 @@ type SendTokensInput = {
|
||||
};
|
||||
|
||||
export function useEngineSendTokens(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useMutation(async (input: SendTokensInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
@@ -1201,7 +1197,7 @@ export function useEngineSendTokens(instance: string) {
|
||||
}
|
||||
|
||||
export function useEngineCorsConfiguration(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.corsUrls(instance),
|
||||
@@ -1225,7 +1221,7 @@ interface SetCorsUrlInput {
|
||||
|
||||
export function useEngineSetCorsConfiguration(instance: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useMutation(
|
||||
async (input: SetCorsUrlInput) => {
|
||||
@@ -1268,7 +1264,7 @@ export interface EngineContractSubscription {
|
||||
}
|
||||
|
||||
export function useEngineContractSubscription(instance: string) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
return useQuery(
|
||||
engineKeys.contractSubscriptions(instance),
|
||||
async () => {
|
||||
@@ -1298,7 +1294,7 @@ export interface AddContractSubscriptionInput {
|
||||
|
||||
export function useEngineAddContractSubscription(instance: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useMutation(
|
||||
async (input: AddContractSubscriptionInput) => {
|
||||
@@ -1333,7 +1329,7 @@ export interface RemoveContractSubscriptionInput {
|
||||
|
||||
export function useEngineRemoveContractSubscription(instance: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useMutation(
|
||||
async (input: RemoveContractSubscriptionInput) => {
|
||||
@@ -1367,7 +1363,7 @@ export function useEngineSubscriptionsLastBlock(
|
||||
chainId: number,
|
||||
autoUpdate: boolean,
|
||||
) {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.contractSubscriptionsLastBlock(instanceUrl, chainId),
|
||||
@@ -1408,7 +1404,6 @@ export function useEngineResourceMetrics(engineId: string) {
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`,
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,53 +1,78 @@
|
||||
import { LOGGED_IN_ONLY_PATHS } from "@/constants/auth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UserWithData } from "@thirdweb-dev/react";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
|
||||
export function fetchUserQuery(address: string) {
|
||||
return {
|
||||
enabled: !!address,
|
||||
queryKey: ["logged_in_user", address],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/user`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch user data");
|
||||
}
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json === null) {
|
||||
throw new Error("Failed to fetch user data");
|
||||
}
|
||||
|
||||
const user = json as UserWithData;
|
||||
if (user && getAddress(user.address) === getAddress(address)) {
|
||||
return user;
|
||||
}
|
||||
throw new Error("Failed to fetch user data");
|
||||
} catch {
|
||||
throw new Error("Failed to fetch user data");
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
} as const;
|
||||
}
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import type { EnsureLoginResponse } from "../../../app/api/auth/ensure-login/route";
|
||||
|
||||
export function useLoggedInUser(): {
|
||||
isLoading: boolean;
|
||||
isLoggedIn: boolean;
|
||||
user: UserWithData | null;
|
||||
user: { address: string; jwt?: string } | null;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const connectedAddress = useActiveAccount()?.address;
|
||||
const query = useQuery(fetchUserQuery(connectedAddress || ""));
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
// this is to work around the fact that `connectionStatus` ends up as "disconnected"
|
||||
// which we *do* care about, but only after we have been "connecting" at least once
|
||||
const statusWasEverConnecting = useRef(false);
|
||||
if (connectionStatus === "connecting" || connectionStatus === "connected") {
|
||||
statusWasEverConnecting.current = true;
|
||||
}
|
||||
|
||||
if (query.fetchStatus === "fetching") {
|
||||
const query = useQuery({
|
||||
// enabled if:
|
||||
// - there is a pathname
|
||||
// - we are not already currently connecting
|
||||
// - the pathname is one of the LOGGED_IN_ONLY_PATHS
|
||||
enabled:
|
||||
!!pathname &&
|
||||
connectionStatus !== "connecting" &&
|
||||
statusWasEverConnecting.current &&
|
||||
LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path)),
|
||||
// the last "persist", part of the queryKey, is to make sure that we do not cache this query in indexDB
|
||||
// convention in v4 of the SDK that we are (ab)using here
|
||||
queryKey: ["logged_in_user", connectedAddress, { persist: false }],
|
||||
retry: false,
|
||||
queryFn: async () => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (pathname) {
|
||||
searchParams.set("pathname", pathname);
|
||||
}
|
||||
if (connectedAddress) {
|
||||
searchParams.set("address", connectedAddress);
|
||||
}
|
||||
const res = await fetch(
|
||||
`/api/auth/ensure-login?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
return (await res.json()) as EnsureLoginResponse;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.redirectTo) {
|
||||
router.replace(data.redirectTo);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// if we are "disconnected" we are not logged in
|
||||
if (connectionStatus === "disconnected") {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
// if we are still connecting, we are "loading"
|
||||
if (connectionStatus === "connecting") {
|
||||
return {
|
||||
isLoading: true,
|
||||
isLoggedIn: false,
|
||||
@@ -55,9 +80,39 @@ export function useLoggedInUser(): {
|
||||
};
|
||||
}
|
||||
|
||||
// same if we do not yet have a path
|
||||
if (!pathname) {
|
||||
return {
|
||||
isLoading: true,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
// if we do not have an address we are not logged in
|
||||
if (!connectedAddress) {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
// if we are not on a logged in path, we can simply return the connected address
|
||||
if (!LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path))) {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoggedIn: true,
|
||||
user: { address: connectedAddress },
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise we return the query data
|
||||
return {
|
||||
isLoading: query.isLoading,
|
||||
isLoggedIn: query.isSuccess ? !!query.data : false,
|
||||
user: query.isSuccess ? query.data : null,
|
||||
isLoggedIn: query.data ? query.data.isLoggedIn : false,
|
||||
user: query.data?.isLoggedIn
|
||||
? { address: connectedAddress, jwt: query.data.jwt }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Star } from "lucide-react";
|
||||
async function favoriteChains() {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/chains/favorites`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
@@ -24,7 +23,6 @@ async function addChainToFavorites(chainId: number) {
|
||||
`${THIRDWEB_API_HOST}/v1/chains/${chainId}/favorite`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
},
|
||||
);
|
||||
const result = await res.json();
|
||||
@@ -36,7 +34,6 @@ async function removeChainFromFavorites(chainId: number) {
|
||||
`${THIRDWEB_API_HOST}/v1/chains/${chainId}/favorite`,
|
||||
{
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
},
|
||||
);
|
||||
const result = await res.json();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "server-only";
|
||||
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { ChainCTAProps } from "./[chain_id]/components/server/cta-card";
|
||||
import baseBanner from "./temp-assets/base-banner.jpeg";
|
||||
@@ -11,6 +10,9 @@ import xaiBanner from "./temp-assets/xai-banner.jpg";
|
||||
import type { ChainMetadataWithServices } from "./types/chain";
|
||||
// END TEMPORARY
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export async function getChains() {
|
||||
const response = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/chains?includeServices=true`,
|
||||
|
||||
+7
-4
@@ -21,13 +21,13 @@ export function useAddPartner(
|
||||
>,
|
||||
) {
|
||||
const { onSuccess, ...queryOptions } = options || {};
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoggedIn, user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: addPartner, isLoading } = useMutation({
|
||||
// Returns the created partner object
|
||||
mutationFn: async (params: AddPartnerParams): Promise<Partner> => {
|
||||
if (!isLoggedIn) {
|
||||
if (!isLoggedIn || !user?.jwt) {
|
||||
throw new Error("Please login to add a partner");
|
||||
}
|
||||
|
||||
@@ -35,8 +35,11 @@ export function useAddPartner(
|
||||
`${params.ecosystem.url}/${params.ecosystem.id}/partner`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user.jwt}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: params.name,
|
||||
allowlistedDomains: params.allowlistedDomains,
|
||||
|
||||
+5
-3
@@ -18,13 +18,13 @@ export function useDeletePartner(
|
||||
>,
|
||||
) {
|
||||
const { onSuccess, ...queryOptions } = options || {};
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoggedIn, user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: deletePartner, isLoading } = useMutation({
|
||||
// Returns true on success
|
||||
mutationFn: async (params: DeletePartnerParams): Promise<boolean> => {
|
||||
if (!isLoggedIn) {
|
||||
if (!isLoggedIn || !user?.jwt) {
|
||||
throw new Error("Please login to delete this partner");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ export function useDeletePartner(
|
||||
`${params.ecosystem.url}/${params.ecosystem.id}/partner/${params.partnerId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Authorization: `Bearer ${user.jwt}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
-1
@@ -16,7 +16,6 @@ export function useEcosystem({
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/ecosystem-wallet/${slug}`,
|
||||
{ credentials: "include" },
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
+5
-3
@@ -3,13 +3,15 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import type { Ecosystem, Partner } from "../../../types";
|
||||
|
||||
export function usePartners({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoggedIn, user } = useLoggedInUser();
|
||||
|
||||
const partnersQuery = useQuery({
|
||||
queryKey: ["ecosystem", ecosystem.id, "partners"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${ecosystem.url}/${ecosystem.id}/partners`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -22,7 +24,7 @@ export function usePartners({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
|
||||
return (await res.json()) as Partner[];
|
||||
},
|
||||
enabled: isLoggedIn,
|
||||
enabled: isLoggedIn && !!user?.jwt,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
|
||||
+6
-4
@@ -18,7 +18,7 @@ export function useUpdateEcosystem(
|
||||
>,
|
||||
) {
|
||||
const { onSuccess, ...queryOptions } = options || {};
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoggedIn, user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
@@ -28,7 +28,7 @@ export function useUpdateEcosystem(
|
||||
} = useMutation({
|
||||
// Returns true if the update was successful
|
||||
mutationFn: async (params: UpdateEcosystemParams): Promise<boolean> => {
|
||||
if (!isLoggedIn) {
|
||||
if (!isLoggedIn || !user?.jwt) {
|
||||
throw new Error("Please login to update this ecosystem");
|
||||
}
|
||||
|
||||
@@ -36,8 +36,10 @@ export function useUpdateEcosystem(
|
||||
`${params.ecosystem.url}/${params.ecosystem.id}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user.jwt}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permission: params.permission,
|
||||
}),
|
||||
|
||||
-1
@@ -40,7 +40,6 @@ export function useCreateEcosystem(
|
||||
`${THIRDWEB_API_HOST}/v1/ecosystem-wallet/add-cloud-hosted`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: params.name,
|
||||
|
||||
+1
-3
@@ -9,9 +9,7 @@ export function useEcosystemList() {
|
||||
const ecosystemQuery = useQuery({
|
||||
queryKey: ["ecosystems", user?.address],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/ecosystem-wallet/list`, {
|
||||
credentials: "include",
|
||||
});
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/ecosystem-wallet/list`);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PayAnalytics } from "components/pay/PayAnalytics/PayAnalytics";
|
||||
import { PayConfig } from "components/pay/PayConfig";
|
||||
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
|
||||
import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TrackedLink } from "tw-components";
|
||||
|
||||
@@ -64,7 +63,7 @@ function usePayConfig() {
|
||||
}
|
||||
|
||||
export default function DashboardConnectPayPage() {
|
||||
const { isLoggedIn, isLoading } = useLoggedInUser();
|
||||
const { isLoading } = useLoggedInUser();
|
||||
const {
|
||||
hasApiKeys,
|
||||
hasPayApiKeys,
|
||||
@@ -83,14 +82,6 @@ export default function DashboardConnectPayPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<div className="py-6 w-full grid place-items-center">
|
||||
<ConnectWalletPrompt description="manage your Pay configuration" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 py-6 w-full">
|
||||
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { decodeJWT, getAddress } from "thirdweb/utils";
|
||||
|
||||
export type EnsureLoginPayload = {
|
||||
pathname: string;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
export type EnsureLoginResponse = {
|
||||
isLoggedIn: boolean;
|
||||
jwt?: string;
|
||||
redirectTo?: string;
|
||||
};
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const address = req.nextUrl.searchParams.get("address");
|
||||
const pathname = req.nextUrl.searchParams.get("pathname");
|
||||
|
||||
const cookieStore = cookies();
|
||||
// if we are "disconnected" we are not logged in, clear the cookie and redirect to login
|
||||
// this is the "log out" case (for now)
|
||||
if (!address) {
|
||||
// delete all cookies that start with the token prefix
|
||||
const allCookies = cookieStore.getAll();
|
||||
for (const cookie of allCookies) {
|
||||
if (cookie.name.startsWith(COOKIE_PREFIX_TOKEN)) {
|
||||
cookieStore.delete(cookie.name);
|
||||
}
|
||||
}
|
||||
// also delete the active account cookie
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(address);
|
||||
|
||||
// check if we have a token
|
||||
const token = cookieStore.get(authCookieName)?.value;
|
||||
|
||||
// if no token, not logged in, redirect to login
|
||||
if (!token) {
|
||||
// delete the active account cookie (the account is not logged in)
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
// otherwise check if the address matches the token
|
||||
const decodedToken = decodeJWT(token);
|
||||
// user is not the same as the token, redirect to login
|
||||
if (getAddress(decodedToken.payload.sub) !== getAddress(address)) {
|
||||
cookieStore.delete(authCookieName);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the active account cookie is set to the correct address
|
||||
const activeAccountCookie = cookieStore.get(COOKIE_ACTIVE_ACCOUNT);
|
||||
if (
|
||||
!activeAccountCookie ||
|
||||
getAddress(activeAccountCookie.value) !== getAddress(address)
|
||||
) {
|
||||
cookieStore.set(COOKIE_ACTIVE_ACCOUNT, getAddress(address), {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
// 3 days
|
||||
maxAge: 3 * 24 * 60 * 60,
|
||||
});
|
||||
}
|
||||
|
||||
// if everything is good simply return true
|
||||
return NextResponse.json({ isLoggedIn: true, jwt: token });
|
||||
};
|
||||
|
||||
function buildLoginPath(pathname?: string | null): string {
|
||||
return `/login${pathname ? `?next=${pathname}` : ""}`;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const POST = async () => {
|
||||
const cookieStore = cookies();
|
||||
|
||||
// delete all cookies that start with the token prefix
|
||||
const allCookies = cookieStore.getAll();
|
||||
for (const cookie of allCookies) {
|
||||
if (cookie.name.startsWith(COOKIE_PREFIX_TOKEN)) {
|
||||
cookieStore.delete(cookie.name);
|
||||
}
|
||||
}
|
||||
// also delete the active account cookie
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
return new Response(null, { status: 200 });
|
||||
};
|
||||
@@ -61,10 +61,7 @@ export function DashboardHeader() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomConnectWallet
|
||||
disableChainConfig={true}
|
||||
disableAddCustomNetwork={true}
|
||||
/>
|
||||
<CustomConnectWallet />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
export function ThirdwebMiniLogo(props: { className?: string }) {
|
||||
import { useId } from "react";
|
||||
|
||||
function prefixId(id: string, prefix?: string) {
|
||||
return prefix ? `${prefix}_${id}` : id;
|
||||
}
|
||||
|
||||
export function ThirdwebMiniLogo(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
<svg viewBox="0 0 516 321" fill="none" className={props.className}>
|
||||
<title>thirdweb</title>
|
||||
<g clipPath="url(#clip0_3:35)">
|
||||
<g clipPath={`url(#${prefixId("clip0_3:35)", id)}`}>
|
||||
<path
|
||||
d="M1.40497 27.0011C-3.73633 14.022 5.84519 0 19.8669 0H106.919C115.098 0 122.342 4.86715 125.381 12.3996L194.671 185.299C196.541 189.935 196.541 195.149 194.671 199.901L151.087 308.484C144.427 325.056 120.823 325.056 114.163 308.484L1.40497 27.0011Z"
|
||||
fill={"url(#paint0_linear_3:35)"}
|
||||
fill={`url(#${prefixId("paint0_linear_3:35)", id)}`}
|
||||
/>
|
||||
<path
|
||||
d="M169.547 26.4217C164.873 13.5585 174.454 0 188.242 0H264.077C272.49 0 279.968 5.2148 282.772 12.9791L345.753 185.879C347.272 190.166 347.272 194.918 345.753 199.321L307.894 303.27C301.585 320.652 276.813 320.652 270.503 303.27L169.547 26.4217Z"
|
||||
fill={"url(#paint1_linear_3:35)"}
|
||||
fill={`url(#${prefixId("paint1_linear_3:35)", id)}`}
|
||||
/>
|
||||
<path
|
||||
d="M321.331 27.0011C316.19 14.022 325.771 0 339.793 0H426.845C435.024 0 442.269 4.86715 445.307 12.3996L514.597 185.299C516.467 189.935 516.467 195.149 514.597 199.901L471.013 308.484C464.353 325.056 440.75 325.056 434.089 308.484L321.331 27.0011Z"
|
||||
fill={"url(#paint2_linear_3:35)"}
|
||||
fill={`url(#${prefixId("paint2_linear_3:35)", id)}`}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_3:35"
|
||||
id={prefixId("paint0_linear_3:35", id)}
|
||||
x1="7.40492"
|
||||
y1="55.24"
|
||||
x2="260.485"
|
||||
@@ -32,7 +41,7 @@ export function ThirdwebMiniLogo(props: { className?: string }) {
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_3:35"
|
||||
id={prefixId("paint1_linear_3:35", id)}
|
||||
x1="175.093"
|
||||
y1="54.447"
|
||||
x2="410.968"
|
||||
@@ -46,7 +55,7 @@ export function ThirdwebMiniLogo(props: { className?: string }) {
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_3:35"
|
||||
id={prefixId("paint2_linear_3:35", id)}
|
||||
x1="327.331"
|
||||
y1="55.24"
|
||||
x2="580.411"
|
||||
@@ -59,7 +68,7 @@ export function ThirdwebMiniLogo(props: { className?: string }) {
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3:35">
|
||||
<clipPath id={prefixId("clip0_3:35", id)}>
|
||||
<rect width="516" height="321" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
@@ -6,8 +6,8 @@ import PlausibleProvider from "next-plausible";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { AppRouterProviders } from "./(dashboard)/providers";
|
||||
import { PostHogProvider } from "./components/root-providers";
|
||||
import { AppRouterProviders } from "./providers";
|
||||
|
||||
const fontSans = interFont({
|
||||
subsets: ["latin"],
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAddress } from "thirdweb";
|
||||
import type {
|
||||
GenerateLoginPayloadParams,
|
||||
LoginPayload,
|
||||
VerifyLoginPayloadParams,
|
||||
} from "thirdweb/auth";
|
||||
import {
|
||||
COOKIE_ACTIVE_ACCOUNT,
|
||||
COOKIE_PREFIX_TOKEN,
|
||||
} from "../../@/constants/cookie";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export async function getLoginPayload(
|
||||
params: GenerateLoginPayloadParams,
|
||||
): Promise<LoginPayload> {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/payload`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
address: params.address,
|
||||
chainId: params.chainId?.toString(),
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error("Failed to fetch login payload", res.status, res.statusText);
|
||||
throw new Error("Failed to fetch login payload");
|
||||
}
|
||||
return (await res.json()).payload;
|
||||
}
|
||||
|
||||
export async function doLogin(
|
||||
payload: VerifyLoginPayloadParams,
|
||||
nextPath?: string | null,
|
||||
) {
|
||||
// forward the request to the API server
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/auth/login`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ payload }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to login - api call failed",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
throw new Error("Failed to login - api call failed");
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.token) {
|
||||
console.error("Failed to login - invalid json", json);
|
||||
throw new Error("Failed to login - invalid json");
|
||||
}
|
||||
|
||||
const cookieStore = cookies();
|
||||
|
||||
// set the token cookie
|
||||
cookieStore.set(
|
||||
COOKIE_PREFIX_TOKEN + getAddress(payload.payload.address),
|
||||
json.token,
|
||||
{
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
// 3 days
|
||||
maxAge: 3 * 24 * 60 * 60,
|
||||
},
|
||||
);
|
||||
|
||||
// set the active account cookie
|
||||
cookieStore.set(COOKIE_ACTIVE_ACCOUNT, getAddress(payload.payload.address), {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
// 3 days
|
||||
maxAge: 3 * 24 * 60 * 60,
|
||||
});
|
||||
|
||||
// redirect to the nextPath (if set)
|
||||
if (nextPath) {
|
||||
return redirect(nextPath);
|
||||
}
|
||||
// if we do not have a next path, redirect to dashboard home
|
||||
return redirect("/dashboard");
|
||||
}
|
||||
|
||||
export async function doLogout() {
|
||||
const cookieStore = cookies();
|
||||
// delete all cookies that start with the token prefix
|
||||
const allCookies = cookieStore.getAll();
|
||||
for (const cookie of allCookies) {
|
||||
if (cookie.name.startsWith(COOKIE_PREFIX_TOKEN)) {
|
||||
cookieStore.delete(cookie.name);
|
||||
}
|
||||
}
|
||||
// also delete the active account cookie
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
}
|
||||
|
||||
export async function isLoggedIn(address: string) {
|
||||
const cookieName = COOKIE_PREFIX_TOKEN + getAddress(address);
|
||||
const cookieStore = cookies();
|
||||
// check if we have an access token
|
||||
const token = cookieStore.get(cookieName)?.value;
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/auth/user`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to check if logged in - api call failed",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
// not logged in
|
||||
// clear the cookie
|
||||
cookieStore.delete(cookieName);
|
||||
return false;
|
||||
}
|
||||
const json = await res.json();
|
||||
|
||||
if (!json) {
|
||||
// not logged in
|
||||
// clear the cookie
|
||||
cookieStore.delete(cookieName);
|
||||
return false;
|
||||
}
|
||||
// if the address matches what we expect, we're logged in
|
||||
if (getAddress(json.address) === getAddress(address)) {
|
||||
// set the active account cookie again
|
||||
cookieStore.set(COOKIE_ACTIVE_ACCOUNT, getAddress(address), {
|
||||
httpOnly: false,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
// 3 days
|
||||
maxAge: 3 * 24 * 60 * 60,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// not logged in
|
||||
// clear the cookie
|
||||
cookieStore.delete(cookieName);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { ColorModeToggle } from "@/components/color-mode-toggle";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { ConnectEmbed } from "thirdweb/react";
|
||||
import { ThirdwebMiniLogo } from "../components/ThirdwebMiniLogo";
|
||||
import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="h-full bg-gradient-to-br from-[#F213A4] to-[#5204BF] p-2 md:p-4 xl:p-6">
|
||||
<main className="h-full grid grid-cols-2 xl:grid-cols-3 rounded-2xl">
|
||||
<div className="col-span-1 hidden xl:grid bg-card place-items-center rounded-l-3xl my-6 shadow-2xl">
|
||||
<ThirdwebLogo key="logo-xl" />
|
||||
</div>
|
||||
<div className="relative col-span-2 grid place-items-center bg-background rounded-3xl shadow-2xl">
|
||||
<div className="absolute top-4 right-4">
|
||||
<ColorModeToggle />
|
||||
</div>
|
||||
<div className="flex flex-col gap-8 items-center ">
|
||||
<ThirdwebLogo key="logo-sm" className="flex xl:hidden" />
|
||||
<Suspense>
|
||||
<CustomConnectEmmbed />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomConnectEmmbed() {
|
||||
const isLG = useMediaQuery("(min-width: 1024px)");
|
||||
const searchParams = useSearchParams();
|
||||
return (
|
||||
<ConnectEmbed
|
||||
auth={{
|
||||
getLoginPayload,
|
||||
doLogin: (params) => doLogin(params, searchParams?.get("next")),
|
||||
doLogout,
|
||||
isLoggedIn,
|
||||
}}
|
||||
client={thirdwebClient}
|
||||
modalSize={isLG ? "wide" : "compact"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ThirdwebLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn("flex flex-row items-center gap-4", className)}>
|
||||
<ThirdwebMiniLogo
|
||||
key="foo"
|
||||
className="size-16 md:size-24 flex-shrink-0"
|
||||
/>
|
||||
<h1 className="text-4xl md:text-5xl font-bold">thirdweb</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useMediaQuery(query: string) {
|
||||
const [matches, setMatches] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia(query).matches;
|
||||
});
|
||||
|
||||
// required here
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(query);
|
||||
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
|
||||
mediaQuery.addEventListener("change", handler);
|
||||
return () => mediaQuery.removeEventListener("change", handler);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { DashboardThirdwebProviderSetup } from "components/app-layouts/provider-setup";
|
||||
import { AllChainsProvider } from "contexts/all-chains";
|
||||
import { ChainsProvider } from "contexts/configured-chains";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { DashboardThirdwebProviderSetup } from "../../components/app-layouts/provider-setup";
|
||||
import { AllChainsProvider } from "../../contexts/all-chains";
|
||||
import { ChainsProvider } from "../../contexts/configured-chains";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -97,19 +97,7 @@ interface AppLayoutProps extends AppShellProps, DashboardThirdwebProviderProps {
|
||||
|
||||
export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
|
||||
// has to be constructed in here because it may otherwise share state between SSR'd pages
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// 24 hours
|
||||
cacheTime: 1000 * 60 * 60 * 24,
|
||||
// 30 seconds
|
||||
staleTime: 1000 * 30,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
|
||||
// will be deleted as part of: https://github.com/thirdweb-dev/dashboard/pull/2648
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -180,7 +168,7 @@ const SanctionedAddressesChecker: ComponentWithChildren = ({ children }) => {
|
||||
>
|
||||
<Flex gap={4} direction="column" align="center">
|
||||
<Heading as="p">Address is blocked</Heading>
|
||||
<CustomConnectWallet noAuth />
|
||||
<CustomConnectWallet />
|
||||
</Flex>
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,12 @@ import {
|
||||
} from "thirdweb/react";
|
||||
import { setThirdwebDomains } from "thirdweb/utils";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
import { THIRDWEB_API_HOST } from "../../constants/urls";
|
||||
|
||||
const THIRDWEB_API_HOST = new URL(
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
|
||||
)
|
||||
.toString()
|
||||
.slice(0, -1);
|
||||
|
||||
// do this upfront
|
||||
setThirdwebDomains({
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import {
|
||||
type ConnectWalletProps,
|
||||
CustomConnectWallet,
|
||||
} from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -20,7 +17,7 @@ import { ChainId, useSDK, useSDKChainId } from "@thirdweb-dev/react";
|
||||
import { BigNumber } from "ethers";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSupportedChain } from "hooks/chains/configureChains";
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { forwardRef, useCallback, useMemo, useRef } from "react";
|
||||
import { VscDebugDisconnect } from "react-icons/vsc";
|
||||
import {
|
||||
useActiveAccount,
|
||||
@@ -44,23 +41,8 @@ function useNetworkMismatchAdapter() {
|
||||
return walletChainId !== v4SDKChainId;
|
||||
}
|
||||
|
||||
export const MismatchButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & ConnectWalletProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
isDisabled,
|
||||
onClick,
|
||||
loadingText,
|
||||
type,
|
||||
upsellTestnet = false,
|
||||
onChainSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, isDisabled, onClick, loadingText, type, ...props }, ref) => {
|
||||
const account = useActiveAccount();
|
||||
const wallet = useActiveWallet();
|
||||
const activeWalletChain = useActiveWalletChain();
|
||||
@@ -77,10 +59,6 @@ export const MismatchButton = forwardRef<
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const chainInfo = useSupportedChain(chainId || -1);
|
||||
|
||||
const hasFaucet =
|
||||
chainInfo &&
|
||||
(chainInfo.chainId === ChainId.Localhost ||
|
||||
(chainInfo.faucets && chainInfo.faucets.length > 0));
|
||||
const eventRef = useRef<React.MouseEvent<HTMLButtonElement, MouseEvent>>();
|
||||
if (!wallet) {
|
||||
return (
|
||||
@@ -156,12 +134,6 @@ export const MismatchButton = forwardRef<
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : !hasFaucet && upsellTestnet && onChainSelect ? (
|
||||
<UpsellTestnetNotice
|
||||
initialFocusRef={initialFocusRef}
|
||||
onClose={onClose}
|
||||
onChainSelect={onChainSelect}
|
||||
/>
|
||||
) : (
|
||||
<NoFundsNotice symbol={chainInfo?.nativeCurrency.symbol || ""} />
|
||||
)}
|
||||
@@ -314,90 +286,3 @@ const NoFundsNotice: React.FC<NoFundsNoticeProps> = ({ symbol }) => {
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const UpsellTestnetNotice: React.FC<{
|
||||
initialFocusRef: React.RefObject<HTMLButtonElement>;
|
||||
onClose: () => void;
|
||||
onChainSelect: (chainId: number) => void;
|
||||
}> = ({ initialFocusRef, onClose, onChainSelect }) => {
|
||||
const trackEvent = useTrack();
|
||||
const connectedChainId = useActiveWalletChain()?.id;
|
||||
const switchNetwork = useSwitchActiveWalletChain();
|
||||
const actuallyCanAttemptSwitch = !!switchNetwork;
|
||||
|
||||
const chain = useSupportedChain(connectedChainId || -1);
|
||||
|
||||
// FIXME: find a better way to track the "show" state of a component
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
trackEvent({
|
||||
category: "no-funds",
|
||||
action: "popover",
|
||||
label: "switch-to-testnet",
|
||||
});
|
||||
}, [trackEvent]);
|
||||
|
||||
const onSwitchWallet = useCallback(async () => {
|
||||
trackEvent({
|
||||
category: "no-funds",
|
||||
action: "click",
|
||||
label: "switch-to-testnet",
|
||||
});
|
||||
if (actuallyCanAttemptSwitch && chain) {
|
||||
await switchNetwork(defineDashboardChain(80002));
|
||||
}
|
||||
onChainSelect(80002);
|
||||
onClose();
|
||||
}, [
|
||||
chain,
|
||||
actuallyCanAttemptSwitch,
|
||||
onClose,
|
||||
switchNetwork,
|
||||
onChainSelect,
|
||||
trackEvent,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap={4}>
|
||||
<Heading size="label.lg">
|
||||
<Flex gap={2} align="center">
|
||||
<Icon boxSize={6} as={AiOutlineWarning} />
|
||||
<span>No funds to deploy</span>
|
||||
</Flex>
|
||||
</Heading>
|
||||
|
||||
<Text>
|
||||
You're trying to deploy to the{" "}
|
||||
<Box as="strong" textTransform="capitalize">
|
||||
{chain?.name}
|
||||
</Box>{" "}
|
||||
network but no funds have been detected.
|
||||
</Text>
|
||||
<Text>
|
||||
You can either get funds on this network or switch to a testnet like
|
||||
Amoy to test your contract.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
ref={actuallyCanAttemptSwitch ? initialFocusRef : undefined}
|
||||
leftIcon={<Icon as={VscDebugDisconnect} />}
|
||||
size="sm"
|
||||
onClick={onSwitchWallet}
|
||||
// isLoading={network.loading}
|
||||
isDisabled={!actuallyCanAttemptSwitch}
|
||||
colorScheme="orange"
|
||||
noOfLines={1}
|
||||
>
|
||||
Switch wallet to Polygon Amoy Testnet
|
||||
</Button>
|
||||
|
||||
{!actuallyCanAttemptSwitch && (
|
||||
<Text size="body.sm" fontStyle="italic">
|
||||
Your connected wallet does not support programmatic switching.
|
||||
<br />
|
||||
Please manually switch the network in your wallet.
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ConnectWalletProps } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import {
|
||||
Center,
|
||||
DarkMode,
|
||||
@@ -33,8 +32,7 @@ import {
|
||||
import { useActiveChainAsDashboardChain } from "../../lib/v5-adapter";
|
||||
import { MismatchButton } from "./MismatchButton";
|
||||
|
||||
interface TransactionButtonProps
|
||||
extends Omit<ConnectWalletProps & ButtonProps, "leftIcon"> {
|
||||
interface TransactionButtonProps extends Omit<ButtonProps, "leftIcon"> {
|
||||
transactionCount: number;
|
||||
isLoading: boolean;
|
||||
isGasless?: boolean;
|
||||
@@ -59,7 +57,6 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
|
||||
colorScheme,
|
||||
variant,
|
||||
isGasless,
|
||||
upsellTestnet,
|
||||
onChainSelect,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
@@ -101,7 +98,6 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<ButtonComponent
|
||||
upsellTestnet={upsellTestnet}
|
||||
onChainSelect={onChainSelect}
|
||||
borderRadius="md"
|
||||
position="relative"
|
||||
|
||||
@@ -57,7 +57,6 @@ export const CreateEngineInstanceButton = ({
|
||||
`${THIRDWEB_API_HOST}/v1/engine/add-cloud-hosted`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useEngineRemoveCloudHosted,
|
||||
useEngineRemoveFromDashboard,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
@@ -45,7 +46,6 @@ import { BiPencil } from "react-icons/bi";
|
||||
import { FiArrowRight, FiTrash } from "react-icons/fi";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Badge, Button, FormLabel, Heading, Text } from "tw-components";
|
||||
import { useAccessToken } from "../../@3rdweb-sdk/react/components/connect-wallet/useAccessToken";
|
||||
|
||||
interface EngineInstancesTableProps {
|
||||
instances: EngineInstance[];
|
||||
@@ -65,7 +65,7 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
const editDisclosure = useDisclosure();
|
||||
const removeDisclosure = useDisclosure();
|
||||
const trackEvent = useTrack();
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const address = useActiveAccount()?.address;
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineInstances,
|
||||
@@ -29,6 +30,13 @@ export const EngineInstancesList = ({
|
||||
const instancesQuery = useEngineInstances();
|
||||
const instances = instancesQuery.data ?? [];
|
||||
|
||||
if (instancesQuery.isLoading) {
|
||||
return (
|
||||
<div className="grid place-items-center">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack spacing={8}>
|
||||
{instances.length === 0 ? (
|
||||
@@ -68,27 +76,18 @@ export const EngineInstancesList = ({
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<Stack>
|
||||
<Heading size="title.lg" as="h1">
|
||||
Engine
|
||||
</Heading>
|
||||
</Stack>
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center">
|
||||
<h1 className="text-4xl font-bold">Engine</h1>
|
||||
<div className="flex flex-row gap-2">
|
||||
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
|
||||
<CreateEngineInstanceButton
|
||||
ctaText="Create Engine Instance"
|
||||
refetch={instancesQuery.refetch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stack spacing={4}>
|
||||
<Flex
|
||||
direction={{ base: "column", md: "row" }}
|
||||
gap={3}
|
||||
justify="flex-end"
|
||||
>
|
||||
<Flex direction={{ base: "column-reverse", md: "row" }} gap={3}>
|
||||
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
|
||||
<CreateEngineInstanceButton
|
||||
ctaText="Create Engine Instance"
|
||||
refetch={instancesQuery.refetch}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<EngineInstancesTable
|
||||
instances={instancesQuery.data ?? ([] as EngineInstance[])}
|
||||
isFetched={instancesQuery.isFetched}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Box, DarkMode } from "@chakra-ui/react";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import "../../../css/swagger-ui.css";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useAccessToken } from "../../../@3rdweb-sdk/react/components/connect-wallet/useAccessToken";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
@@ -14,7 +14,7 @@ interface EngineExplorerProps {
|
||||
export const EngineExplorer: React.FC<EngineExplorerProps> = ({
|
||||
instanceUrl,
|
||||
}) => {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
return (
|
||||
<ClientOnly ssr={null}>
|
||||
<DarkMode>
|
||||
|
||||
@@ -93,7 +93,6 @@ const ModalImportEngine = ({
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import {
|
||||
Center,
|
||||
Divider,
|
||||
Flex,
|
||||
Icon,
|
||||
ListItem,
|
||||
Stack,
|
||||
UnorderedList,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { VscBook } from "react-icons/vsc";
|
||||
import { Card, Heading, Link, Text } from "tw-components";
|
||||
|
||||
export const EngineNoConnectedWallet: React.FC = () => {
|
||||
return (
|
||||
<Stack spacing={8}>
|
||||
<Center>
|
||||
<Stack spacing={8}>
|
||||
<ChakraNextImage
|
||||
alt="Engine hero image"
|
||||
src={require("../../../public/assets/engine/empty-state-header.png")}
|
||||
maxW={650}
|
||||
/>
|
||||
<Heading size="title.lg" as="h1">
|
||||
Your scalable web3 backend server
|
||||
</Heading>
|
||||
|
||||
<UnorderedList color="gray.600">
|
||||
<ListItem>
|
||||
Read, write, and deploy contracts at production scale
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Reliably parallelize and retry transactions with gas & nonce
|
||||
management
|
||||
</ListItem>
|
||||
<ListItem>Securely manage backend wallets</ListItem>
|
||||
<ListItem>
|
||||
Built-in support for account abstraction, relayers, and more
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<Flex h="fit-content">
|
||||
<CustomConnectWallet />
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card p={8}>
|
||||
<Stack>
|
||||
<Flex gap={2} align="center">
|
||||
<Icon as={VscBook} />
|
||||
<Heading size="title.xs">Learn more about Engine</Heading>
|
||||
</Flex>
|
||||
<Text>Dive into features and integration guides.</Text>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/engine"
|
||||
isExternal
|
||||
color="blue.500"
|
||||
fontSize="small"
|
||||
>
|
||||
View Docs →
|
||||
</Link>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Transaction } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
@@ -22,7 +23,6 @@ import { useRef } from "react";
|
||||
import { FiCheck } from "react-icons/fi";
|
||||
import { Button, FormLabel, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "tw-components/AddressCopyButton";
|
||||
import { useAccessToken } from "../../../@3rdweb-sdk/react/components/connect-wallet/useAccessToken";
|
||||
|
||||
interface TimelineStep {
|
||||
step: string;
|
||||
@@ -182,7 +182,7 @@ const CancelTransactionButton = ({
|
||||
instanceUrl: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Successfully sent a request to cancel the transaction",
|
||||
"Failed to cancel transaction",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AccountForm } from "components/settings/Account/AccountForm";
|
||||
import { useState } from "react";
|
||||
import { useActiveWallet, useDisconnect } from "thirdweb/react";
|
||||
import { Button } from "tw-components";
|
||||
import { logout } from "../../@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { OnboardingTitle } from "./Title";
|
||||
|
||||
type OnboardingGeneralProps = {
|
||||
@@ -24,7 +23,6 @@ export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
|
||||
async function handleDisconnect() {
|
||||
if (activeWallet) {
|
||||
await logout();
|
||||
disconnect(activeWallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useLoggedInUser } from "../../../../@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { THIRDWEB_PAY_DOMAIN } from "../../../../constants/urls";
|
||||
|
||||
export type PayTopCustomersData = {
|
||||
count: number;
|
||||
@@ -28,27 +27,30 @@ export function usePayCustomers(options: {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["usePayCustomers", user?.address, options],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const endpoint = new URL(
|
||||
const endpoint =
|
||||
options.type === "new-customers"
|
||||
? `https://${THIRDWEB_PAY_DOMAIN}/stats/new-customers/v1`
|
||||
: `https://${THIRDWEB_PAY_DOMAIN}/stats/customers/v1`,
|
||||
);
|
||||
? "/stats/new-customers/v1"
|
||||
: "/stats/customers/v1";
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
const start = options.pageSize * pageParam;
|
||||
endpoint.searchParams.append("skip", `${start}`);
|
||||
endpoint.searchParams.append("take", `${options.pageSize}`);
|
||||
searchParams.append("skip", `${start}`);
|
||||
searchParams.append("take", `${options.pageSize}`);
|
||||
|
||||
endpoint.searchParams.append("clientId", options.clientId);
|
||||
endpoint.searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
endpoint.searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
searchParams.append("clientId", options.clientId);
|
||||
searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
|
||||
const res = await fetch(endpoint.toString(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`/api/server-proxy/pay/${endpoint}?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch pay volume");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLoggedInUser } from "../../../../@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { THIRDWEB_PAY_DOMAIN } from "../../../../constants/urls";
|
||||
|
||||
export type PayNewCustomersData = {
|
||||
intervalType: "day" | "week";
|
||||
@@ -35,21 +34,21 @@ export function usePayNewCustomers(options: {
|
||||
return useQuery(
|
||||
["usePayNewCustomers", user?.address, options],
|
||||
async () => {
|
||||
const endpoint = new URL(
|
||||
`https://${THIRDWEB_PAY_DOMAIN}/stats/aggregate/customers/v1`,
|
||||
);
|
||||
endpoint.searchParams.append("intervalType", options.intervalType);
|
||||
endpoint.searchParams.append("clientId", options.clientId);
|
||||
endpoint.searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
endpoint.searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
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(endpoint.toString(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`/api/server-proxy/pay/stats/aggregate/customers/v1?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch new customers");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLoggedInUser } from "../../../../@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { THIRDWEB_PAY_DOMAIN } from "../../../../constants/urls";
|
||||
|
||||
export type PayPurchasesData = {
|
||||
count: number;
|
||||
@@ -73,21 +72,23 @@ export function usePayPurchases(options: PayPurcaseOptions) {
|
||||
}
|
||||
|
||||
export async function getPayPurchases(options: PayPurcaseOptions) {
|
||||
const endpoint = new URL(`https://${THIRDWEB_PAY_DOMAIN}/stats/purchases/v1`);
|
||||
endpoint.searchParams.append("skip", `${options.start}`);
|
||||
endpoint.searchParams.append("take", `${options.count}`);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("skip", `${options.start}`);
|
||||
searchParams.append("take", `${options.count}`);
|
||||
|
||||
endpoint.searchParams.append("clientId", options.clientId);
|
||||
endpoint.searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
endpoint.searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
searchParams.append("clientId", options.clientId);
|
||||
searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
|
||||
const res = await fetch(endpoint.toString(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`/api/server-proxy/pay/stats/purchases/v1?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch pay volume");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLoggedInUser } from "../../../../@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { THIRDWEB_PAY_DOMAIN } from "../../../../constants/urls";
|
||||
|
||||
type AggregatedData = {
|
||||
succeeded: {
|
||||
@@ -66,21 +65,21 @@ export function usePayVolume(options: {
|
||||
return useQuery(
|
||||
["usePayVolume", user?.address, options],
|
||||
async () => {
|
||||
const endpoint = new URL(
|
||||
`https://${THIRDWEB_PAY_DOMAIN}/stats/aggregate/volume/v1`,
|
||||
);
|
||||
endpoint.searchParams.append("intervalType", options.intervalType);
|
||||
endpoint.searchParams.append("clientId", options.clientId);
|
||||
endpoint.searchParams.append("fromDate", `${options.from.getTime()}`);
|
||||
endpoint.searchParams.append("toDate", `${options.to.getTime()}`);
|
||||
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(endpoint.toString(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`/api/server-proxy/pay/stats/aggregate/volume/v1?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch pay volume");
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
|
||||
interface ConnectWalletPromptProps {
|
||||
title?: string;
|
||||
prefix?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const ConnectWalletPrompt: React.FC<ConnectWalletPromptProps> = ({
|
||||
title = "Connect your wallet",
|
||||
prefix = "Sign-in with your wallet to",
|
||||
description = "",
|
||||
}) => {
|
||||
return (
|
||||
<div className="container max-w-[600px]">
|
||||
<Card className="flex flex-col gap-2 p-6">
|
||||
<h2 className="text-lg font-bold">{title}</h2>
|
||||
<p>
|
||||
{prefix} {description || "get started"}.
|
||||
</p>
|
||||
<div className="border-b my-4" />
|
||||
<CustomConnectWallet />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import { DASHBOARD_STORAGE_URL } from "lib/sdk";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button, Card, Heading, Text, TrackedCopyButton } from "tw-components";
|
||||
import { toSize } from "utils/number";
|
||||
import { useAccessToken } from "../../@3rdweb-sdk/react/components/connect-wallet/useAccessToken";
|
||||
|
||||
interface PinnedFilesResponse {
|
||||
result: PinnedFilesResult;
|
||||
@@ -37,7 +36,6 @@ function usePinnedFilesQuery({
|
||||
pageSize: number;
|
||||
}) {
|
||||
const user = useLoggedInUser();
|
||||
const token = useAccessToken();
|
||||
|
||||
const offset = page * pageSize;
|
||||
|
||||
@@ -54,7 +52,7 @@ function usePinnedFilesQuery({
|
||||
if (!user.isLoggedIn) {
|
||||
throw new Error("User is not logged in");
|
||||
}
|
||||
if (!token) {
|
||||
if (!user.user?.jwt) {
|
||||
throw new Error("No token");
|
||||
}
|
||||
const res = await fetch(
|
||||
@@ -63,20 +61,20 @@ function usePinnedFilesQuery({
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${user.user.jwt}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
return (await res.json()) as PinnedFilesResponse;
|
||||
},
|
||||
enabled: user.isLoggedIn && !!user.user?.address && !!token,
|
||||
enabled: user.isLoggedIn && !!user.user?.address && !!user.user.jwt,
|
||||
// keep the previous data when fetching new data
|
||||
keepPreviousData: true,
|
||||
});
|
||||
}
|
||||
|
||||
function useUnpinFileMutation() {
|
||||
const token = useAccessToken();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ cid }: { cid: string }) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export const THIRDWEB_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_DOMAIN || "thirdweb.com";
|
||||
|
||||
export const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
export const THIRDWEB_API_HOST = "/api/server-proxy/api";
|
||||
|
||||
export const THIRDWEB_EWS_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST || "https://ews.thirdweb.com";
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { LOGGED_IN_ONLY_PATHS } from "@/constants/auth";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
// middleware.ts
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb";
|
||||
|
||||
// ignore assets, api - only intercept page routes
|
||||
export const config = {
|
||||
@@ -18,6 +21,25 @@ export const config = {
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const authCookie = activeAccount
|
||||
? request.cookies.get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))
|
||||
: null;
|
||||
|
||||
// logged in paths
|
||||
if (LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path))) {
|
||||
// check if the user is logged in (has a valid auth cookie)
|
||||
|
||||
if (!authCookie) {
|
||||
// if not logged in, rewrite to login page
|
||||
return redirect(request, "/login", `next=${pathname}`, false);
|
||||
}
|
||||
}
|
||||
// /login redirect
|
||||
if (pathname === "/login" && authCookie) {
|
||||
// if the user is logged in, redirect to dashboard
|
||||
return redirect(request, "/dashboard");
|
||||
}
|
||||
|
||||
// remove '/' in front and then split by '/'
|
||||
const paths = pathname.slice(1).split("/");
|
||||
@@ -32,6 +54,7 @@ export function middleware(request: NextRequest) {
|
||||
return redirect(
|
||||
request,
|
||||
`/thirdweb.eth/${paths.slice(1).join("/")}`,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -63,9 +86,11 @@ function rewrite(request: NextRequest, relativePath: string) {
|
||||
function redirect(
|
||||
request: NextRequest,
|
||||
relativePath: string,
|
||||
searchParams?: string,
|
||||
permanent = false,
|
||||
) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = relativePath;
|
||||
url.search = searchParams ? `?${searchParams}` : "";
|
||||
return NextResponse.redirect(url, permanent ? 308 : undefined);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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\/api/, "");
|
||||
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
searchParams.delete("paths");
|
||||
|
||||
// create a new URL object for the API server
|
||||
const API_SERVER_URL = new URL(
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.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",
|
||||
// pass the auth token if we have one
|
||||
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
||||
},
|
||||
body: req.body,
|
||||
});
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
AccountStatus,
|
||||
type ApiKey,
|
||||
@@ -20,7 +21,6 @@ import { AppLayout } from "components/app-layouts/app";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
|
||||
import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SmartWallets } from "components/smart-wallets";
|
||||
import { ConnectSidebar } from "core-ui/sidebar/connect";
|
||||
import { getAbsoluteUrl } from "lib/vercel-utils";
|
||||
@@ -48,7 +48,7 @@ const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
const router = useRouter();
|
||||
const defaultTabIndex = Number.parseInt(router.query.tab?.toString() || "0");
|
||||
const defaultClientId = router.query.clientId?.toString();
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoading } = useLoggedInUser();
|
||||
const keysQuery = useApiKeys();
|
||||
const [selectedKey_, setSelectedKey] = useState<undefined | ApiKey>();
|
||||
const meQuery = useAccount();
|
||||
@@ -94,8 +94,12 @@ const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
|
||||
};
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="manage accounts" />;
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid w-full place-items-center">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -160,7 +164,7 @@ const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isLoggedIn && hasSmartWalletsWithoutBilling ? (
|
||||
{hasSmartWalletsWithoutBilling ? (
|
||||
<SmartWalletsBillingAlert />
|
||||
) : (
|
||||
<Alert
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
type ApiKey,
|
||||
useApiKeys,
|
||||
@@ -22,7 +23,6 @@ import { ChartContainer } from "components/analytics/chart-container";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { GatedFeature } from "components/settings/Account/Billing/GatedFeature";
|
||||
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { ConnectSidebar } from "core-ui/sidebar/connect";
|
||||
import { useRouter } from "next/router";
|
||||
import { PageId } from "page-id";
|
||||
@@ -45,7 +45,7 @@ const DashboardConnectAnalytics: ThirdwebNextPage = () => {
|
||||
const defaultClientId = router.query.clientId?.toString();
|
||||
const { colorMode } = useColorMode();
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoading } = useLoggedInUser();
|
||||
const keysQuery = useApiKeys();
|
||||
const [selectedKey_, setSelectedKey] = useState<undefined | ApiKey>();
|
||||
|
||||
@@ -234,9 +234,11 @@ const DashboardConnectAnalytics: ThirdwebNextPage = () => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ConnectWalletPrompt description="view wallet analytics for your apps" />
|
||||
<div className="grid w-full place-items-center">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { type ApiKey, useApiKeys } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
@@ -6,7 +7,6 @@ import { AppLayout } from "components/app-layouts/app";
|
||||
import { EmbeddedWallets } from "components/embedded-wallets";
|
||||
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
|
||||
import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { ConnectSidebar } from "core-ui/sidebar/connect";
|
||||
import { useRouter } from "next/router";
|
||||
import { PageId } from "page-id";
|
||||
@@ -22,7 +22,7 @@ const DashboardConnectEmbeddedWallets: ThirdwebNextPage = () => {
|
||||
const router = useRouter();
|
||||
const defaultTabIndex = Number.parseInt(router.query.tab?.toString() || "0");
|
||||
const defaultClientId = router.query.clientId?.toString();
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoading } = useLoggedInUser();
|
||||
const keysQuery = useApiKeys();
|
||||
|
||||
const [selectedKey_, setSelectedKey] = useState<undefined | ApiKey>();
|
||||
@@ -55,8 +55,12 @@ const DashboardConnectEmbeddedWallets: ThirdwebNextPage = () => {
|
||||
|
||||
const wallets = walletsQuery?.data || [];
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="manage in-app wallets" />;
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid w-full place-items-center">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { EngineInstancesList } from "components/engine/engine-list";
|
||||
import { EngineNavigation } from "components/engine/engine-navigation";
|
||||
import { EngineNoConnectedWallet } from "components/engine/no-connected-wallet";
|
||||
import { SidebarNav } from "core-ui/sidebar/nav";
|
||||
|
||||
import { PageId } from "page-id";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const EngineManage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const [connectedInstance, setConnectedInstance] = useState<
|
||||
EngineInstance | undefined
|
||||
>();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<>
|
||||
{/* just placeholder, so it doesn't look empty */}
|
||||
<SidebarNav title="Engine" />
|
||||
<EngineNoConnectedWallet />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedInstance) {
|
||||
return (
|
||||
// contains its own sidebarnav component
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { ApiKeyDetails } from "components/settings/ApiKeys/Details";
|
||||
import { EditApiKey } from "components/settings/ApiKeys/Edit";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { useRouter } from "next/router";
|
||||
import { PageId } from "page-id";
|
||||
@@ -29,7 +28,7 @@ const SettingsApiKeyPage: ThirdwebNextPage = () => {
|
||||
}, [apiKey, keysQuery.isSuccess, router]);
|
||||
|
||||
if (!user) {
|
||||
return <ConnectWalletPrompt />;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
|
||||
@@ -3,13 +3,11 @@ import {
|
||||
useAccount,
|
||||
useApiKeys,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { ApiKeys } from "components/settings/ApiKeys";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { CreateApiKeyButton } from "components/settings/ApiKeys/Create";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { PageId } from "page-id";
|
||||
import { useMemo } from "react";
|
||||
@@ -17,7 +15,6 @@ import { Heading, Link, Text } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const SettingsApiKeysPage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const keysQuery = useApiKeys();
|
||||
const meQuery = useAccount();
|
||||
|
||||
@@ -37,10 +34,6 @@ const SettingsApiKeysPage: ThirdwebNextPage = () => {
|
||||
);
|
||||
}, [apiKeys, account]);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="manage your developer API keys" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={8}>
|
||||
<Flex direction="column" gap={2}>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { Billing } from "components/settings/Account/Billing";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { useRouter } from "next/router";
|
||||
import { PageId } from "page-id";
|
||||
@@ -10,7 +8,6 @@ import { useEffect } from "react";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const SettingsBillingPage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn, isLoading } = useLoggedInUser();
|
||||
const meQuery = useAccount({
|
||||
refetchInterval: (account) =>
|
||||
[
|
||||
@@ -34,10 +31,6 @@ const SettingsBillingPage: ThirdwebNextPage = () => {
|
||||
}
|
||||
}, [payment_intent, router, source_redirect_slug]);
|
||||
|
||||
if (!isLoading && !isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="claim your free 1 month trial." />;
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { useAuthorizedWallets } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { AuthorizedWalletsTable } from "components/settings/AuthorizedWallets/AuthorizedWalletsTable";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { PageId } from "page-id";
|
||||
import { Heading, Text } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const SettingsDevicesPage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const authorizedWalletsQuery = useAuthorizedWallets();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="view your authorized devices" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={8}>
|
||||
<Flex direction="column" gap={2}>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { ApplyForOpCreditsModal } from "components/onboarding/ApplyForOpCreditsModal";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { PageId } from "page-id";
|
||||
import { Heading, LinkButton } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
import { ApplyForOpCreditsModal } from "../../../components/onboarding/ApplyForOpCreditsModal";
|
||||
|
||||
const SettingsGasCreditsPage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const { isLoading } = useLoggedInUser();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="gas credits" />;
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid w-full place-items-center">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { Notifications } from "components/settings/Account/Notifications";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { PageId } from "page-id";
|
||||
import { Heading, Text } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const SettingsNotificationsPage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const meQuery = useAccount();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<ConnectWalletPrompt description="configure your notification preferences" />
|
||||
);
|
||||
}
|
||||
|
||||
if (meQuery.isLoading || !meQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { useAccount, useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Flex, HStack } from "@chakra-ui/react";
|
||||
import { AppLayout } from "components/app-layouts/app";
|
||||
import { BillingPeriod } from "components/settings/Account/Billing/Period";
|
||||
import { BillingPlan } from "components/settings/Account/Billing/Plan";
|
||||
import { Usage } from "components/settings/Account/Usage";
|
||||
import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt";
|
||||
import { SettingsSidebar } from "core-ui/sidebar/settings";
|
||||
import { PageId } from "page-id";
|
||||
import { Heading } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const SettingsUsagePage: ThirdwebNextPage = () => {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const meQuery = useAccount();
|
||||
const usageQuery = useAccountUsage();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <ConnectWalletPrompt description="view usage analytics" />;
|
||||
}
|
||||
|
||||
if (meQuery.isLoading || !meQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { deccodeJWT } from "../../utils/jwt/decode-jwt.js";
|
||||
import { decodeJWT } from "../../utils/jwt/decode-jwt.js";
|
||||
import type { JWTPayload } from "../../utils/jwt/types.js";
|
||||
import { verifyEOASignature } from "../verify-signature.js";
|
||||
import type { AuthOptions } from "./types.js";
|
||||
@@ -26,7 +26,7 @@ export type VerifyJWTResult =
|
||||
*/
|
||||
export function verifyJWT(options: AuthOptions) {
|
||||
return async (params: VerifyJWTParams): Promise<VerifyJWTResult> => {
|
||||
const { payload, signature } = deccodeJWT(params.jwt);
|
||||
const { payload, signature } = decodeJWT(params.jwt);
|
||||
|
||||
if (!options.adminAccount) {
|
||||
throw new Error("No admin account provided. Cannot verify JWT.");
|
||||
|
||||
@@ -149,3 +149,11 @@ export { stringify } from "../utils/json.js";
|
||||
// values
|
||||
// ------------------------------------------------
|
||||
export { maxUint256 } from "viem";
|
||||
|
||||
// ------------------------------------------------
|
||||
// jwt
|
||||
// ------------------------------------------------
|
||||
export { decodeJWT } from "../utils/jwt/decode-jwt.js";
|
||||
export { encodeJWT, type JWTPayloadInput } from "../utils/jwt/encode-jwt.js";
|
||||
export { refreshJWT, type RefreshJWTParams } from "../utils/jwt/refresh-jwt.js";
|
||||
export type { JWTPayload } from "../utils/jwt/types.js";
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { JWTPayload } from "./types.js";
|
||||
* const { payload, signature } = decodeJWT(jwt);
|
||||
* ```
|
||||
*/
|
||||
export function deccodeJWT(jwt: string) {
|
||||
export function decodeJWT(jwt: string) {
|
||||
const [encodedHeader, encodedPayload, encodedSignature] = jwt.split(".");
|
||||
|
||||
if (encodedHeader !== PRECOMPILED_B64_ENCODED_JWT_HEADER) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Account } from "../../wallets/interfaces/wallet.js";
|
||||
import { deccodeJWT } from "./decode-jwt.js";
|
||||
import { decodeJWT } from "./decode-jwt.js";
|
||||
import { encodeJWT } from "./encode-jwt.js";
|
||||
|
||||
const DEFAULT_EXPIRATION_TIME = 60 * 60 * 24;
|
||||
@@ -27,7 +27,7 @@ export type RefreshJWTParams = {
|
||||
*/
|
||||
export async function refreshJWT(options: RefreshJWTParams): Promise<string> {
|
||||
const { account, jwt, expirationTime = DEFAULT_EXPIRATION_TIME } = options;
|
||||
const payload = deccodeJWT(jwt).payload;
|
||||
const payload = decodeJWT(jwt).payload;
|
||||
return encodeJWT({
|
||||
payload: {
|
||||
iss: payload.iss,
|
||||
|
||||
Reference in New Issue
Block a user