<!-- 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:
jnsdls
2024-07-08 06:17:38 +00:00
parent d253bad72f
commit 342d74efff
65 changed files with 847 additions and 758 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
export JWT utils, fix naming of `decodeJWT`
+7
View File
@@ -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
];
+2
View File
@@ -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`,
@@ -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,
@@ -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}`,
},
},
);
@@ -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) {
@@ -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,
});
@@ -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,
}),
@@ -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,
@@ -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>
+1 -1
View File
@@ -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;
}
+80
View File
@@ -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;
}
@@ -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&apos;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 &amp; 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 &rarr;
</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 -2
View File
@@ -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";
+25
View File
@@ -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.");
+8
View File
@@ -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,