diff --git a/.changeset/funny-dragons-thank.md b/.changeset/funny-dragons-thank.md
new file mode 100644
index 00000000000..b7fa4e1ae74
--- /dev/null
+++ b/.changeset/funny-dragons-thank.md
@@ -0,0 +1,5 @@
+---
+"@thirdweb-dev/service-utils": patch
+---
+
+Update TeamResponse and ProjectResponse types
diff --git a/apps/dashboard/src/@/api/projects.ts b/apps/dashboard/src/@/api/projects.ts
index f45ecf02496..9683f205f7e 100644
--- a/apps/dashboard/src/@/api/projects.ts
+++ b/apps/dashboard/src/@/api/projects.ts
@@ -1,23 +1,9 @@
import "server-only";
import { API_SERVER_URL } from "@/constants/env";
+import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
-export type Project = {
- id: string;
- name: string;
- createdAt: Date;
- updatedAt: Date;
- deletedAt: Date | null;
- bannedAt: Date | null;
- domains: string[];
- bundleIds: string[];
- redirectUrls: string[];
- lastAccessedAt: Date | null;
- slug: string;
- teamId: string;
- publishableKey: string;
- // image: string; // TODO
-};
+export type Project = ProjectResponse;
export async function getProjects(teamSlug: string) {
const token = await getAuthToken();
diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts
index 5e3823ca011..efdb350da83 100644
--- a/apps/dashboard/src/@/api/team.ts
+++ b/apps/dashboard/src/@/api/team.ts
@@ -1,35 +1,9 @@
import "server-only";
import { API_SERVER_URL } from "@/constants/env";
+import type { TeamResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
-type EnabledTeamScope =
- | "pay"
- | "storage"
- | "rpc"
- | "bundler"
- | "insight"
- | "embeddedWallets"
- | "relayer"
- | "chainsaw"
- | "nebula";
-
-export type Team = {
- id: string;
- name: string;
- slug: string;
- createdAt: string;
- updatedAt: string;
- deletedAt?: string;
- bannedAt?: string;
- image?: string;
- billingPlan: "pro" | "growth" | "free" | "starter";
- billingStatus: "validPayment" | (string & {}) | null;
- supportPlan: "pro" | "growth" | "free" | "starter";
- billingEmail: string | null;
- growthTrialEligible: false;
- enabledScopes: EnabledTeamScope[];
-};
-
+export type Team = TeamResponse;
export async function getTeamBySlug(slug: string) {
const token = await getAuthToken();
diff --git a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts
index accbcf2fa95..ee3b5ca6f84 100644
--- a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts
+++ b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts
@@ -42,15 +42,6 @@ export const accountKeys = {
[...accountKeys.wallet(walletAddress), "billing-session"] as const,
};
-export const apiKeys = {
- all: ["api"] as const,
- wallet: (walletAddress: string) => [...apiKeys.all, walletAddress] as const,
- keys: (walletAddress: string) =>
- [...apiKeys.wallet(walletAddress), "keys"] as const,
- key: (id: string, walletAddress: string) =>
- [...apiKeys.keys(walletAddress), id] as const,
-};
-
export const authorizedWallets = {
all: ["authorizedWallets"] as const,
wallet: (walletAddress: string) =>
diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
index cfb6b1c753b..926077f6925 100644
--- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
+++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
@@ -1,22 +1,14 @@
import { analyticsServerProxy, apiServerProxy } from "@/actions/proxies";
+import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAllChainsData } from "hooks/chains/allChains";
import { useActiveAccount } from "thirdweb/react";
import type { UserOpStats } from "types/analytics";
-import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
+import { accountKeys, authorizedWallets } from "../cache-keys";
// FIXME: We keep repeating types, API server should provide them
-export const accountStatus = {
- noCustomer: "noCustomer",
- noPayment: "noPayment",
- paymentVerification: "paymentVerification",
- validPayment: "validPayment",
- invalidPayment: "invalidPayment",
- invalidPaymentMethod: "invalidPaymentMethod",
-} as const;
-
export const accountPlan = {
free: "free",
growth: "growth",
@@ -71,109 +63,6 @@ interface ConfirmEmailInput {
confirmationToken: string;
}
-type ApiKeyRecoverShareManagement = "AWS_MANAGED" | "USER_MANAGED";
-type ApiKeyCustomAuthentication = {
- jwksUri: string;
- aud: string;
-};
-type ApiKeyCustomAuthEndpoint = {
- authEndpoint: string;
- customHeaders: { key: string; value: string }[];
-};
-
-// MAP to api-server types in PolicyService.ts
-export type ApiKeyServicePolicy = {
- allowedChainIds?: number[] | null;
- allowedContractAddresses?: string[] | null;
- allowedWallets?: string[] | null;
- blockedWallets?: string[] | null;
- bypassWallets?: string[] | null;
- serverVerifier?: {
- url: string;
- headers: { key: string; value: string }[] | null;
- } | null;
- limits?: ApiKeyServicePolicyLimits | null;
-};
-
-export type ApiKeyServicePolicyLimits = {
- global?: {
- // in dollars or ETH
- maxSpend: string;
- maxSpendUnit: "usd" | "native";
- } | null;
- // ----------------------
- // TODO implement perUser limits
- perUserSpend?: {
- // in dollars or ETH
- maxSpend: string | null;
- maxSpendUnit: "usd" | "native";
- maxSpendPeriod: "day" | "week" | "month";
- } | null;
- perUserTransactions?: {
- maxTransactions: number;
- maxTransactionsPeriod: "day" | "week" | "month";
- } | null;
-};
-
-export type ApiKeyService = {
- id: string;
- name: string;
- targetAddresses: string[];
- actions: string[];
- // If updating here, need to update validation logic in `validation.ts` as well for recoveryShareManagement
- // EMBEDDED WALLET
- recoveryShareManagement?: ApiKeyRecoverShareManagement;
- customAuthentication?: ApiKeyCustomAuthentication;
- customAuthEndpoint?: ApiKeyCustomAuthEndpoint;
- applicationName?: string;
- applicationImageUrl?: string;
- // PAY
- payoutAddress?: string;
-};
-
-export type ApiKey = {
- id: string;
- name: string;
- key: string;
- secret?: string;
- secretMasked: string;
- accountId: string;
- creatorWalletAddress: string;
- walletAddresses: string[];
- domains: string[];
- bundleIds: string[];
- redirectUrls: string[];
- revokedAt: string;
- lastAccessedAt: string;
- createdAt: string;
- updatedAt: string;
- services?: ApiKeyService[];
-};
-
-interface UpdateKeyServiceInput {
- name: string;
- targetAddresses: string[];
- actions?: string[];
-}
-
-export interface CreateKeyInput {
- name?: string;
- domains?: string[];
- bundleIds?: string[];
- walletAddresses?: string[];
- services?: UpdateKeyServiceInput[];
-}
-
-export interface UpdateKeyInput {
- id: string;
- name: string;
- domains: string[];
- bundleIds: string[];
- walletAddresses?: string[];
- services?: UpdateKeyServiceInput[];
- redirectUrls: string[];
-}
-
interface UsageStorage {
sumFileSizeBytes: number;
}
@@ -461,9 +350,6 @@ export function useUpdateNotifications() {
}
export function useConfirmEmail() {
- const address = useActiveAccount()?.address;
- const queryClient = useQueryClient();
-
return useMutation({
mutationFn: async (input: ConfirmEmailInput) => {
type Result = {
@@ -492,20 +378,6 @@ export function useConfirmEmail() {
return json.data;
},
- onSuccess: async () => {
- // invalidate related cache, since could be relinking account
- return Promise.all([
- queryClient.invalidateQueries({
- queryKey: apiKeys.keys(address || ""),
- }),
- queryClient.invalidateQueries({
- queryKey: accountKeys.usage(address || ""),
- }),
- queryClient.invalidateQueries({
- queryKey: accountKeys.me(address || ""),
- }),
- ]);
- },
});
}
@@ -549,205 +421,106 @@ export function useResendEmailConfirmation() {
});
}
-export function useCreateApiKey() {
- const address = useActiveAccount()?.address;
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (input: CreateKeyInput) => {
- type Result = {
- data: ApiKey;
- error?: { message: string };
- };
-
- const res = await apiServerProxy({
- pathname: "/v1/keys",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(input),
- });
-
- if (!res.ok) {
- throw new Error(res.error);
- }
-
- const json = res.data;
-
- if (json.error) {
- throw new Error(json.error.message);
- }
+export async function createProjectClient(
+ teamId: string,
+ body: Partial,
+) {
+ type Response = {
+ result: {
+ project: Project;
+ secret: string;
+ };
+ };
- return json.data;
- },
- onSuccess: () => {
- return queryClient.invalidateQueries({
- queryKey: apiKeys.keys(address || ""),
- });
+ const res = await apiServerProxy({
+ pathname: `/v1/teams/${teamId}/projects`,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
},
+ body: JSON.stringify(body),
});
-}
-
-export function useUpdateApiKey() {
- const address = useActiveAccount()?.address;
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (input: UpdateKeyInput) => {
- type Result = {
- data: ApiKey;
- error?: { message: string };
- };
- const res = await apiServerProxy({
- pathname: `/v1/keys/${input.id}`,
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(input),
- });
-
- if (!res.ok) {
- throw new Error(res.error);
- }
+ if (!res.ok) {
+ throw new Error(res.error);
+ }
- const json = res.data;
+ return res.data.result;
+}
- if (json.error) {
- throw new Error(json.error.message);
- }
+export async function updateProjectClient(
+ params: {
+ projectId: string;
+ teamId: string;
+ },
+ body: Partial,
+) {
+ type Response = {
+ result: Project;
+ };
- return json.data;
- },
- onSuccess: () => {
- return queryClient.invalidateQueries({
- queryKey: apiKeys.keys(address || ""),
- });
+ const res = await apiServerProxy({
+ pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}`,
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
},
+ body: JSON.stringify(body),
});
-}
-
-export function useRevokeApiKey() {
- const address = useActiveAccount()?.address;
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (id: string) => {
- type Result = {
- data: ApiKey;
- error?: { message: string };
- };
-
- const res = await apiServerProxy({
- pathname: `/v1/keys/${id}/revoke`,
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({}),
- });
- if (!res.ok) {
- throw new Error(res.error);
- }
+ if (!res.ok) {
+ throw new Error(res.error);
+ }
- const json = res.data;
+ return res.data.result;
+}
- if (json.error) {
- throw new Error(json.error.message);
- }
+export async function deleteProjectClient(params: {
+ projectId: string;
+ teamId: string;
+}) {
+ type Response = {
+ result: true;
+ };
- return json.data;
- },
- onSuccess: () => {
- return queryClient.invalidateQueries({
- queryKey: apiKeys.keys(address || ""),
- });
- },
+ const res = await apiServerProxy({
+ pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}`,
+ method: "DELETE",
});
-}
-export const usePolicies = (serviceId?: string) => {
- return useQuery({
- queryKey: ["policies", serviceId],
- queryFn: async () => {
- if (!serviceId) {
- throw new Error();
- }
-
- type Result = {
- data: ApiKeyServicePolicy;
- error?: { message: string };
- };
+ if (!res.ok) {
+ throw new Error(res.error);
+ }
- const res = await apiServerProxy({
- pathname: "/v1/policies",
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- searchParams: {
- serviceId,
- },
- });
+ return res.data.result;
+}
- if (!res.ok) {
- throw new Error(res.error);
- }
+export type RotateSecretKeyAPIReturnType = {
+ data: {
+ secret: string;
+ secretMasked: string;
+ secretHash: string;
+ };
+};
- const json = res.data;
- if (json.error) {
- throw new Error(json.error.message);
- }
- return json.data;
+export async function rotateSecretKeyClient(projectId: string) {
+ const res = await apiServerProxy({
+ pathname: "/v2/keys/rotate-secret-key",
+ method: "POST",
+ body: JSON.stringify({
+ projectId,
+ }),
+ headers: {
+ "Content-Type": "application/json",
},
- enabled: !!serviceId,
});
-};
-
-export const useUpdatePolicies = () => {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: async (input: {
- serviceId: string;
- data: ApiKeyServicePolicy;
- }) => {
- type Result = {
- data: ApiKeyServicePolicy;
- error?: { message: string };
- };
- const res = await apiServerProxy({
- pathname: "/v1/policies",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- serviceId: input.serviceId,
- data: input.data,
- }),
- });
-
- if (!res.ok) {
- throw new Error(res.error);
- }
+ if (!res.ok) {
+ throw new Error(res.error);
+ }
- const json = res.data;
- if (json.error) {
- throw new Error(json.error.message);
- }
- return json.data;
- },
- onSuccess: (_, variables) => {
- return queryClient.invalidateQueries({
- queryKey: ["policies", variables.serviceId],
- });
- },
- });
-};
+ return res.data;
+}
export function useRevokeAuthorizedWallet() {
const address = useActiveAccount()?.address;
diff --git a/apps/dashboard/src/app/account/components/AccountHeader.tsx b/apps/dashboard/src/app/account/components/AccountHeader.tsx
index e596a59901d..2dba27098d5 100644
--- a/apps/dashboard/src/app/account/components/AccountHeader.tsx
+++ b/apps/dashboard/src/app/account/components/AccountHeader.tsx
@@ -8,7 +8,7 @@ import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { useCallback, useState } from "react";
import { useActiveWallet, useDisconnect } from "thirdweb/react";
-import { LazyCreateAPIKeyDialog } from "../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
+import { LazyCreateProjectDialog } from "../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { doLogout } from "../../login/auth-actions";
import {
type AccountHeaderCompProps,
@@ -61,27 +61,26 @@ export function AccountHeader(props: {
-
- setCreateProjectDialogState({
- isOpen: false,
- })
- }
- onCreateAndComplete={() => {
- // refresh projects
- router.refresh();
- }}
- teamSlug={
- createProjectDialogState.isOpen
- ? createProjectDialogState.team.slug
- : undefined
- }
- enableNebulaServiceByDefault={
- createProjectDialogState.isOpen &&
- createProjectDialogState.team.enabledScopes.includes("nebula")
- }
- />
+ {createProjectDialogState.isOpen && (
+
+ setCreateProjectDialogState({
+ isOpen: false,
+ })
+ }
+ onCreateAndComplete={() => {
+ // refresh projects
+ router.refresh();
+ }}
+ teamId={createProjectDialogState.team.id}
+ teamSlug={createProjectDialogState.team.slug}
+ enableNebulaServiceByDefault={
+ createProjectDialogState.isOpen &&
+ createProjectDialogState.team.enabledScopes.includes("nebula")
+ }
+ />
+ )}
);
}
diff --git a/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx b/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
index 3f891178292..acd60f27c19 100644
--- a/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
+++ b/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
@@ -108,7 +108,7 @@ function TeamRow(props: {
diff --git a/apps/dashboard/src/app/api/lib/getAPIKeys.ts b/apps/dashboard/src/app/api/lib/getAPIKeys.ts
deleted file mode 100644
index c10e7d8e577..00000000000
--- a/apps/dashboard/src/app/api/lib/getAPIKeys.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { API_SERVER_URL } from "@/constants/env";
-import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
-import { getAuthToken } from "./getAuthToken";
-
-// TODO - Fix the `/v1/keys/${apiKeyId}` endpoint in API server
-
-async function getAPIKey(apiKeyId: string) {
- // The `/v1/keys/${apiKeyId}`; does not return the "FULL" ApiKey object for some reason
- // Until this is fixed in API server - we just use the getApiKeys() and find the key by id
-
- const apiKeys = await getApiKeys();
- return apiKeys.find((key) => key.id === apiKeyId);
-
- // const authToken = getAuthToken();
- // const apiServerURL = new URL(
- // process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
- // );
-
- // apiServerURL.pathname = `/v1/keys/${apiKeyId}`;
-
- // const res = await fetch(apiServerURL, {
- // method: "GET",
- // headers: {
- // Authorization: `Bearer ${authToken}`,
- // },
- // });
-
- // const json = await res.json();
-
- // if (json.error) {
- // console.error(json.error);
- // return undefined;
- // }
-
- // return json.data as ApiKey;
-}
-
-async function getApiKeys() {
- const authToken = await getAuthToken();
-
- const res = await fetch(`${API_SERVER_URL}/v1/keys`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${authToken}`,
- },
- });
- const json = await res.json();
-
- if (json.error) {
- return [];
- }
-
- return json.data as ApiKey[];
-}
-
-export function getAPIKeyForProjectId(projectId: string) {
- if (projectId.startsWith("prj_")) {
- return getAPIKey(projectId.slice("prj_".length));
- }
-
- return getAPIKey(projectId);
-}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
index 5fd0c305525..2a3881c7007 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
@@ -24,7 +24,7 @@ import {
SelectTrigger,
} from "@/components/ui/select";
import { useDashboardRouter } from "@/lib/DashboardRouter";
-import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
+import { LazyCreateProjectDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import {
ChevronDownIcon,
EllipsisVerticalIcon,
@@ -167,10 +167,11 @@ export function TeamProjectsPage(props: {
)}
- {
// refresh projects
router.refresh();
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/credits/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/credits/page.tsx
index a14b024caf8..7c092fa46cc 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/credits/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/credits/page.tsx
@@ -16,7 +16,7 @@ export default async function Page(props: {
const team = await getTeamBySlug(params.team_slug);
if (!team) {
- return redirect("/team");
+ redirect("/team");
}
return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx
index 6866d0a566f..e4a6f3cfaa1 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx
@@ -156,12 +156,12 @@ function TeamSlugFormControl(props: {
function TeamAvatarFormControl(props: {
updateTeamImage: (file: File | undefined) => Promise;
- avatar: string | undefined;
+ avatar: string | null;
client: ThirdwebClient;
}) {
const teamAvatarUrl = resolveSchemeWithErrorHandler({
client: props.client,
- uri: props.avatar,
+ uri: props.avatar || undefined,
});
const [teamAvatar, setTeamAvatar] = useState();
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
index b5c5f0d02b1..a320aa5ad39 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
@@ -2,14 +2,9 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { TabPathLinks } from "@/components/ui/tabs";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
-import {
- type ApiKeyService,
- accountStatus,
- useUserOpUsageAggregate,
-} from "@3rdweb-sdk/react/hooks/useApi";
+import { useUserOpUsageAggregate } from "@3rdweb-sdk/react/hooks/useApi";
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
import { CircleAlertIcon } from "lucide-react";
-import { useMemo } from "react";
import { useActiveWalletChain } from "thirdweb/react";
import { AccountAbstractionSummary } from "../../../../../../components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { AAFooterSection } from "./AAFooterSection";
@@ -21,22 +16,11 @@ export function AccountAbstractionLayout(props: {
projectSlug: string;
teamSlug: string;
projectKey: string;
- apiKeyServices: ApiKeyService[];
- billingStatus: "validPayment" | (string & {}) | null;
children: React.ReactNode;
+ hasSmartWalletsWithoutBilling: boolean;
}) {
- const { apiKeyServices } = props;
-
const chain = useActiveWalletChain();
- const hasSmartWalletsWithoutBilling = useMemo(() => {
- return apiKeyServices.find(
- (s) =>
- props.billingStatus !== accountStatus.validPayment &&
- s.name === "bundler",
- );
- }, [apiKeyServices, props.billingStatus]);
-
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
const smartWalletsLayoutSlug = `/team/${props.teamSlug}/${props.projectSlug}/connect/account-abstraction`;
@@ -65,7 +49,7 @@ export function AccountAbstractionLayout(props: {
- {hasSmartWalletsWithoutBilling ? (
+ {props.hasSmartWalletsWithoutBilling ? (
) : (
isOpChain && (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/layout.tsx
index d7d25d38da4..b4250c02371 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/layout.tsx
@@ -1,9 +1,8 @@
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import type { Metadata } from "next";
-import { notFound, redirect } from "next/navigation";
+import { redirect } from "next/navigation";
import { getAbsoluteUrl } from "../../../../../../lib/vercel-utils";
-import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
import { AccountAbstractionLayout } from "./AccountAbstractionPage";
export default async function Page(props: {
@@ -22,22 +21,22 @@ export default async function Page(props: {
}
if (!project) {
- redirect("/team");
+ redirect(`/team/${team_slug}`);
}
- const apiKey = await getAPIKeyForProjectId(project.id);
+ const isBundlerServiceEnabled = !!project.services.find(
+ (s) => s.name === "bundler",
+ );
- if (!apiKey) {
- notFound();
- }
+ const hasSmartWalletsWithoutBilling =
+ isBundlerServiceEnabled && team.billingStatus !== "validPayment";
return (
{props.children}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx
index a5469326bf2..15abe26c62b 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx
@@ -1,36 +1,57 @@
import { getProject } from "@/api/projects";
+import { getTeamBySlug } from "@/api/team";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
-import { notFound } from "next/navigation";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { CircleAlertIcon } from "lucide-react";
+import { redirect } from "next/navigation";
import { AccountAbstractionSettingsPage } from "../../../../../../../components/smart-wallets/SponsorshipPolicies";
-import { getValidAccount } from "../../../../../../account/settings/getAccount";
-import { getAPIKeyForProjectId } from "../../../../../../api/lib/getAPIKeys";
+import { getValidTeamPlan } from "../../../../../components/TeamHeader/getValidTeamPlan";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const { team_slug, project_slug } = await props.params;
- const [account, project] = await Promise.all([
- getValidAccount(),
+ const [project, team] = await Promise.all([
getProject(team_slug, project_slug),
+ getTeamBySlug(team_slug),
]);
+ if (!team) {
+ redirect("/team");
+ }
+
if (!project) {
- notFound();
+ redirect(`/team/${team_slug}`);
}
- const apiKey = await getAPIKeyForProjectId(project.id);
+ const bundlerService = project.services.find((s) => s.name === "bundler");
- if (!apiKey) {
- notFound();
+ if (!bundlerService) {
+ return (
+
+
+ Account Abstraction service is disabled
+
+ Enable Account Abstraction service in{" "}
+
+ project settings
+ {" "}
+ to configure the sponsorship rules
+
+
+ );
}
return (
);
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
index 7415baefc07..ab61df4bc07 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
@@ -1,7 +1,6 @@
import { getProject } from "@/api/projects";
-import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
-import { notFound } from "next/navigation";
-import { TabPathLinks } from "../../../../../../@/components/ui/tabs";
+import { TabPathLinks } from "@/components/ui/tabs";
+import { redirect } from "next/navigation";
import { InAppWalletFooterSection } from "./_components/footer";
import { InAppWalletsHeader } from "./_components/header";
import { TRACKING_CATEGORY } from "./_constants";
@@ -13,24 +12,18 @@ export default async function Layout(props: {
}>;
children: React.ReactNode;
}) {
- const project = await getProject(
- (await props.params).team_slug,
- (await props.params).project_slug,
- );
- if (!project) {
- notFound();
- }
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
- const apiKey = await getAPIKeyForProjectId(project.id);
- if (!apiKey) {
- notFound();
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
}
- const { team_slug, project_slug } = await props.params;
+ const { team_slug, project_slug } = params;
return (
-
+
;
@@ -13,8 +12,11 @@ export default async function Page(props: {
interval?: string;
}>;
}) {
- const searchParams = await props.searchParams;
- const params = await props.params;
+ const [searchParams, params] = await Promise.all([
+ props.searchParams,
+ props.params,
+ ]);
+
const range =
searchParams.from && searchParams.to
? {
@@ -31,18 +33,14 @@ export default async function Page(props: {
: "week";
const project = await getProject(params.team_slug, params.project_slug);
- if (!project) {
- notFound();
- }
- const apiKey = await getAPIKeyForProjectId(project.id);
- if (!apiKey) {
- notFound();
+ if (!project) {
+ redirect(`/team/${params.team_slug}`);
}
return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx
index b9a63aa003f..11968dcc72e 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx
@@ -1,35 +1,34 @@
import { getProject } from "@/api/projects";
-import { notFound, redirect } from "next/navigation";
+import { getTeamBySlug } from "@/api/team";
+import { redirect } from "next/navigation";
import { InAppWalletSettingsPage } from "../../../../../../../components/embedded-wallets/Configure";
-import { getValidAccount } from "../../../../../../account/settings/getAccount";
-import { getAPIKeyForProjectId } from "../../../../../../api/lib/getAPIKeys";
+import { getValidTeamPlan } from "../../../../../components/TeamHeader/getValidTeamPlan";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const { team_slug, project_slug } = await props.params;
- const [account, project] = await Promise.all([
- getValidAccount(),
+ const [team, project] = await Promise.all([
+ getTeamBySlug(team_slug),
getProject(team_slug, project_slug),
]);
- if (!project) {
+ if (!team) {
redirect("/team");
}
- const apiKey = await getAPIKeyForProjectId(project.id);
-
- if (!apiKey) {
- // unexpected error - this should never happen
- notFound();
+ if (!project) {
+ redirect(`/team/${team_slug}`);
}
return (
);
}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx
index 612d6980d19..1e165bca55d 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx
@@ -21,7 +21,7 @@ export default async function Page(props: {
}
if (!project) {
- redirect("/team");
+ redirect(`/team/${params.team_slug}`);
}
return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/layout.tsx
index b0525234027..d3dae20f557 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/layout.tsx
@@ -1,7 +1,7 @@
import { getProject } from "@/api/projects";
import { TabPathLinks } from "@/components/ui/tabs";
import Link from "next/link";
-import { notFound } from "next/navigation";
+import { redirect } from "next/navigation";
export default async function Layout(props: {
params: Promise<{
@@ -14,7 +14,7 @@ export default async function Layout(props: {
const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
- notFound();
+ redirect(`/team/${params.team_slug}`);
}
const payLayoutPath = `/team/${params.team_slug}/${params.project_slug}/connect/pay`;
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/page.tsx
index f33c5cd6bb2..4f945d1bf72 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/page.tsx
@@ -1,5 +1,5 @@
import { getProject } from "@/api/projects";
-import { notFound } from "next/navigation";
+import { redirect } from "next/navigation";
import { PayAnalytics } from "../../../../../../components/pay/PayAnalytics/PayAnalytics";
export default async function Page(props: {
@@ -8,13 +8,11 @@ export default async function Page(props: {
project_slug: string;
}>;
}) {
- const project = await getProject(
- (await props.params).team_slug,
- (await props.params).project_slug,
- );
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
- notFound();
+ redirect(`/team/${params.team_slug}`);
}
return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/settings/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/settings/page.tsx
index 298d6597b73..1b7b793aba7 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/settings/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/settings/page.tsx
@@ -1,7 +1,7 @@
import { getProject } from "@/api/projects";
-import { notFound } from "next/navigation";
+import { getTeamBySlug } from "@/api/team";
+import { redirect } from "next/navigation";
import { PayConfig } from "../../../../../../../components/pay/PayConfig";
-import { getAPIKeyForProjectId } from "../../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: Promise<{
@@ -10,17 +10,19 @@ export default async function Page(props: {
}>;
}) {
const { team_slug, project_slug } = await props.params;
- const project = await getProject(team_slug, project_slug);
- if (!project) {
- notFound();
- }
+ const [project, team] = await Promise.all([
+ getProject(team_slug, project_slug),
+ getTeamBySlug(team_slug),
+ ]);
- const apiKey = await getAPIKeyForProjectId(project.id);
+ if (!team) {
+ redirect("/team");
+ }
- if (!apiKey) {
- notFound();
+ if (!project) {
+ redirect(`/team/${team_slug}`);
}
- return ;
+ return ;
}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/page.tsx
index a7c16cc5684..824f6310ff3 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/pay/webhooks/page.tsx
@@ -1,5 +1,5 @@
import { getProject } from "@/api/projects";
-import { notFound } from "next/navigation";
+import { redirect } from "next/navigation";
import { PayWebhooksPage } from "./components/webhooks.client";
export default async function Page(props: {
@@ -8,13 +8,11 @@ export default async function Page(props: {
project_slug: string;
}>;
}) {
- const project = await getProject(
- (await props.params).team_slug,
- (await props.params).project_slug,
- );
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
- notFound();
+ redirect(`/team/${params.team_slug}`);
}
return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
index af6f6ac8027..b281cd8617b 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
@@ -1,6 +1,6 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
-import { notFound, redirect } from "next/navigation";
+import { redirect } from "next/navigation";
import { getValidAccount } from "../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
@@ -27,8 +27,7 @@ export default async function TeamLayout(props: {
);
if (!team) {
- // not a valid team, redirect back to 404
- notFound();
+ redirect("/team");
}
const teamsAndProjects = await Promise.all(
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx
index 77614ed0c28..95f17015504 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx
@@ -1,6 +1,6 @@
import { type Project, getProject } from "@/api/projects";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
-import { notFound } from "next/navigation";
+import { redirect } from "next/navigation";
import {
type DurationId,
@@ -68,7 +68,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
};
if (!project) {
- notFound();
+ redirect(`/team/${params.team_slug}`);
}
const isActive = await isProjectActive({ clientId: project.publishableKey });
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
index af88097f2de..1920edab966 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
@@ -1,8 +1,6 @@
-import type { UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
import type { Meta, StoryObj } from "@storybook/react";
-import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
-import { createApiKeyStub } from "../../../../../stories/stubs";
+import { projectStub } from "../../../../../stories/stubs";
import { mobileViewport } from "../../../../../stories/utils";
import { ProjectGeneralSettingsPageUI } from "./ProjectGeneralSettingsPage";
@@ -30,35 +28,21 @@ export const Mobile: Story = {
},
};
-const apiKeyStub = createApiKeyStub();
-apiKeyStub.secret = undefined;
-
function Story() {
- const updateMutation = useMutation({
- mutationFn: async (inputs: UpdateKeyInput) => {
- await new Promise((resolve) => setTimeout(resolve, 1000));
- console.log("update with", inputs);
- },
- });
-
- const deleteMutation = useMutation({
- mutationFn: async (id: string) => {
- await new Promise((resolve) => setTimeout(resolve, 1000));
- console.log("delete with", id);
- },
- });
return (
{
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("updateProject", params);
+ return projectStub("foo", "team-1");
+ }}
+ deleteProject={async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("deleteProject");
}}
+ project={projectStub("foo", "team-1")}
+ teamSlug="foo"
onKeyUpdated={undefined}
rotateSecretKey={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
index 441cdf9f563..0ed74c41bc1 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
@@ -1,6 +1,5 @@
"use client";
-
-import { apiServerProxy } from "@/actions/proxies";
+import type { Project } from "@/api/projects";
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
@@ -24,13 +23,15 @@ import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
-import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
+import type { RotateSecretKeyAPIReturnType } from "@3rdweb-sdk/react/hooks/useApi";
import {
- useRevokeApiKey,
- useUpdateApiKey,
+ deleteProjectClient,
+ rotateSecretKeyClient,
+ updateProjectClient,
} from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
-import { type UseMutationResult, useMutation } from "@tanstack/react-query";
+import { useMutation } from "@tanstack/react-query";
+import type { ProjectService } from "@thirdweb-dev/service-utils";
import { SERVICES } from "@thirdweb-dev/service-utils";
import {
type ServiceName,
@@ -48,199 +49,210 @@ import { useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
import { toast } from "sonner";
+import { RE_BUNDLE_ID } from "utils/regex";
import { joinWithComma, toArrFromList } from "utils/string";
+import { validStrList } from "utils/validations";
+import { z } from "zod";
import {
HIDDEN_SERVICES,
- type ProjectSettingsPageFormSchema,
- projectSettingsPageFormSchema,
+ projectDomainsSchema,
+ projectNameSchema,
} from "../../../../../components/settings/ApiKeys/validations";
-type EditProjectUIPaths = {
+// TODO: instead of single submit handler, move the submit to each section
+
+const projectSettingsFormSchema = z.object({
+ name: projectNameSchema,
+ domains: projectDomainsSchema,
+ servicesMeta: z.array(
+ z.object({
+ name: z.string(),
+ enabled: z.boolean(),
+ actions: z.array(z.string()),
+ }),
+ ),
+ bundleIds: z.string().refine((str) => validStrList(str, RE_BUNDLE_ID), {
+ message: "Some of the bundle ids are invalid",
+ }),
+});
+
+type ProjectSettingsPageFormSchema = z.infer;
+
+type ProjectSettingPaths = {
inAppConfig: string;
aaConfig: string;
payConfig: string;
afterDeleteRedirectTo: string;
};
-type RotateSecretKeyAPIReturnType = {
- data: {
- secret: string;
- secretMasked: string;
- secretHash: string;
- };
-};
-
export function ProjectGeneralSettingsPage(props: {
- apiKey: ApiKey;
- paths: EditProjectUIPaths;
- onKeyUpdated: (() => void) | undefined;
+ project: Project;
+ teamSlug: string;
showNebulaSettings: boolean;
- projectId: string;
}) {
- const updateMutation = useUpdateApiKey();
- const deleteMutation = useRevokeApiKey();
+ const router = useDashboardRouter();
return (
{
- const res = await apiServerProxy({
- pathname: "/v2/keys/rotate-secret-key",
- method: "POST",
- body: JSON.stringify({
- projectId: props.projectId,
- }),
- headers: {
- "Content-Type": "application/json",
+ teamSlug={props.teamSlug}
+ project={props.project}
+ updateProject={async (projectValues) => {
+ return updateProjectClient(
+ {
+ projectId: props.project.id,
+ teamId: props.project.teamId,
},
+ projectValues,
+ );
+ }}
+ deleteProject={async () => {
+ await deleteProjectClient({
+ projectId: props.project.id,
+ teamId: props.project.teamId,
});
-
- if (!res.ok) {
- throw new Error(res.error);
- }
-
- return res.data;
+ }}
+ onKeyUpdated={() => {
+ router.refresh();
+ }}
+ showNebulaSettings={props.showNebulaSettings}
+ rotateSecretKey={async () => {
+ return rotateSecretKeyClient(props.project.id);
}}
/>
);
}
-type UpdateMutation = UseMutationResult<
- unknown,
- unknown,
- UpdateKeyInput,
- unknown
->;
-
-type DeleteMutation = UseMutationResult;
+type UpdateProject = (project: Partial) => Promise;
+type DeleteProject = () => Promise;
+type RotateSecretKey = () => Promise;
+type UpdateAPIForm = UseFormReturn;
-interface EditApiKeyProps {
- apiKey: ApiKey;
- updateMutation: UpdateMutation;
- deleteMutation: DeleteMutation;
- paths: EditProjectUIPaths;
+export function ProjectGeneralSettingsPageUI(props: {
+ project: Project;
+ updateProject: UpdateProject;
+ deleteProject: DeleteProject;
onKeyUpdated: (() => void) | undefined;
showNebulaSettings: boolean;
- rotateSecretKey: () => Promise;
-}
+ rotateSecretKey: RotateSecretKey;
+ teamSlug: string;
+}) {
+ const projectLayout = `/team/${props.teamSlug}/${props.project.slug}`;
-type UpdateAPIForm = UseFormReturn;
+ const paths = {
+ aaConfig: `${projectLayout}/connect/account-abstraction/settings`,
+ inAppConfig: `${projectLayout}/connect/in-app-wallets/settings`,
+ payConfig: `${projectLayout}/connect/pay/settings`,
+ afterDeleteRedirectTo: `/team/${props.teamSlug}`,
+ };
-export const ProjectGeneralSettingsPageUI: React.FC = (
- props,
-) => {
- const { apiKey, updateMutation, deleteMutation } = props;
+ const { project } = props;
const trackEvent = useTrack();
const router = useDashboardRouter();
+ const updateProject = useMutation({
+ mutationFn: props.updateProject,
+ });
+
const form = useForm({
- resolver: zodResolver(projectSettingsPageFormSchema),
+ resolver: zodResolver(projectSettingsFormSchema),
defaultValues: {
- name: apiKey.name,
- domains: joinWithComma(apiKey.domains),
- bundleIds: joinWithComma(apiKey.bundleIds),
- redirectUrls: joinWithComma(apiKey.redirectUrls),
- services: SERVICES.map((srv) => {
- const existingService = (apiKey.services || []).find(
- (s) => s.name === srv.name,
+ name: project.name,
+ domains: joinWithComma(project.domains),
+ bundleIds: joinWithComma(project.bundleIds),
+ servicesMeta: SERVICES.map((service) => {
+ const projectService = project.services.find(
+ (projectService) => projectService.name === service.name,
);
return {
- name: srv.name,
- targetAddresses: existingService
- ? joinWithComma(existingService.targetAddresses)
- : "",
- enabled: !!existingService,
- actions: existingService?.actions || [],
- recoveryShareManagement: existingService?.recoveryShareManagement,
- customAuthentication: existingService?.customAuthentication,
- customAuthEndpoint: existingService?.customAuthEndpoint,
- applicationName: existingService?.applicationName,
- applicationImageUrl: existingService?.applicationImageUrl,
+ name: service.name as ServiceName,
+ enabled: !!projectService,
+ actions: projectService?.actions || [],
};
}),
},
});
const handleSubmit = form.handleSubmit((values) => {
- const enabledServices = (values.services || []).filter(
- (srv) => !!srv.enabled,
- );
+ const services: ProjectService[] = [];
+
+ for (const serviceMeta of values.servicesMeta) {
+ if (serviceMeta.enabled) {
+ function getBaseService(): ProjectService {
+ const projectService = project.services.find(
+ (s) => s.name === serviceMeta.name,
+ );
+
+ if (projectService) {
+ return projectService;
+ }
+
+ if (serviceMeta.name === "pay") {
+ return {
+ name: "pay",
+ payoutAddress: null,
+ actions: [],
+ };
+ }
+
+ return {
+ name: serviceMeta.name as Exclude,
+ actions: [],
+ };
+ }
- if (enabledServices.length > 0) {
- // validate embedded wallets custom auth
- const embeddedWallets = enabledServices.find(
- (s) => s.name === "embeddedWallets",
- );
-
- if (embeddedWallets) {
- const { customAuthentication, recoveryShareManagement } =
- embeddedWallets;
-
- if (
- recoveryShareManagement === "USER_MANAGED" &&
- (!customAuthentication?.aud.length ||
- !customAuthentication?.jwksUri.length)
- ) {
- return toast.error("Custom JSON Web Token configuration is invalid", {
- description:
- "To use In-App Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
- });
+ const serviceToAdd = getBaseService();
+
+ // add the actions changes to the base service
+ if (serviceMeta.name === "storage") {
+ serviceToAdd.actions = serviceMeta.actions as ("read" | "write")[];
+ services.push(serviceToAdd);
}
- }
- const formattedValues = {
- id: apiKey.id,
- name: values.name,
- domains: toArrFromList(values.domains),
- bundleIds: toArrFromList(values.bundleIds),
- redirectUrls: toArrFromList(values.redirectUrls, true),
- services: (values.services || [])
- .filter((srv) => srv.enabled)
- // FIXME: Not yet supported, add when it is
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- .map(({ recoveryShareManagement, ...srv }) => ({
- ...srv,
- targetAddresses: toArrFromList(srv.targetAddresses),
- })),
- };
-
- trackEvent({
- category: "api-keys",
- action: "edit",
- label: "attempt",
- });
+ services.push(serviceToAdd);
+ }
+ }
- updateMutation.mutate(formattedValues, {
- onSuccess: () => {
- toast.success("Project updated successfully");
- trackEvent({
- category: "api-keys",
- action: "edit",
- label: "success",
- });
-
- props.onKeyUpdated?.();
- },
- onError: (err) => {
- toast.error("Failed to update project");
- trackEvent({
- category: "api-keys",
- action: "edit",
- label: "error",
- error: err,
- });
- },
- });
- } else {
- toast.error("Service not selected", {
- description: "Choose at least one service",
+ if (services.length === 0) {
+ return toast.error("No services selected", {
+ description: "Please select at least one service",
});
}
+
+ const projectValues: Partial = {
+ id: project.id,
+ name: values.name,
+ domains: toArrFromList(values.domains),
+ bundleIds: toArrFromList(values.bundleIds),
+ services,
+ };
+
+ trackEvent({
+ category: "api-keys",
+ action: "edit",
+ label: "attempt",
+ });
+
+ updateProject.mutate(projectValues, {
+ onSuccess: () => {
+ toast.success("Project updated successfully");
+ trackEvent({
+ category: "api-keys",
+ action: "edit",
+ label: "success",
+ });
+
+ props.onKeyUpdated?.();
+ },
+ onError: (err) => {
+ toast.error("Failed to update project");
+ trackEvent({
+ category: "api-keys",
+ action: "edit",
+ label: "error",
+ error: err,
+ });
+ },
+ });
});
return (
@@ -255,56 +267,54 @@ export const ProjectGeneralSettingsPageUI: React.FC = (
-
{
- router.replace(props.paths.afterDeleteRedirectTo);
+ router.replace(paths.afterDeleteRedirectTo);
}}
/>
);
-};
+}
function ProjectNameSetting(props: {
form: UpdateAPIForm;
- updateMutation: UpdateMutation;
+ isUpdatingProject: boolean;
handleSubmit: () => void;
}) {
- const { form, updateMutation, handleSubmit } = props;
+ const { form, handleSubmit } = props;
const isNameDirty = form.getFieldState("name").isDirty;
return (
@@ -319,7 +329,7 @@ function ProjectNameSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isNameDirty,
- isPending: updateMutation.isPending && isNameDirty,
+ isPending: props.isUpdatingProject && isNameDirty,
}}
bottomText="Please use 64 characters at maximum"
>
@@ -336,10 +346,10 @@ function ProjectNameSetting(props: {
function AllowedDomainsSetting(props: {
form: UpdateAPIForm;
- updateMutation: UpdateMutation;
+ isUpdatingProject: boolean;
handleSubmit: () => void;
}) {
- const { form, handleSubmit, updateMutation } = props;
+ const { form, handleSubmit } = props;
const isDomainsDirty = form.getFieldState("domains").isDirty;
const helperText = (
@@ -384,7 +394,7 @@ function AllowedDomainsSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !isDomainsDirty,
- isPending: updateMutation.isPending && isDomainsDirty,
+ isPending: props.isUpdatingProject && isDomainsDirty,
}}
bottomText="This is only applicable for web applications"
>
@@ -441,17 +451,17 @@ function AllowedDomainsSetting(props: {
function AllowedBundleIDsSetting(props: {
form: UpdateAPIForm;
- updateMutation: UpdateMutation;
+ isUpdatingProject: boolean;
handleSubmit: () => void;
}) {
- const { form, handleSubmit, updateMutation } = props;
+ const { form, handleSubmit } = props;
const isBundleIdsDirty = form.getFieldState("bundleIds").isDirty;
return (
void;
- apiKey: ApiKey;
- paths: EditApiKeyProps["paths"];
+ paths: ProjectSettingPaths;
showNebulaSettings: boolean;
}) {
- const { form, handleSubmit, updateMutation } = props;
+ const { form, handleSubmit } = props;
- const { fields, update } = useFieldArray({
+ const formFields = useFieldArray({
control: form.control,
- name: "services",
+ name: "servicesMeta",
});
- const handleAction = (
+
+ const toggleServiceAction = (
srvIdx: number,
- srv: FieldArrayWithId,
+ srv: FieldArrayWithId,
actionName: string,
checked: boolean,
) => {
@@ -538,7 +548,7 @@ function EnabledServicesSetting(props: {
? [...(srv.actions || []), actionName]
: (srv.actions || []).filter((a) => a !== actionName);
- update(srvIdx, {
+ formFields.update(srvIdx, {
...srv,
actions,
});
@@ -555,19 +565,25 @@ function EnabledServicesSetting(props: {
saveButton={{
onClick: handleSubmit,
disabled: !form.formState.isDirty,
- isPending: updateMutation.isPending,
+ isPending: props.isUpdatingProject,
}}
bottomText=""
>
- {fields.map((srv, idx) => {
- const service = getServiceByName(srv.name as ServiceName);
+ {formFields.fields.map((service, idx) => {
+ const serviceDefinition = getServiceByName(
+ service.name as ServiceName,
+ );
+
const hidden =
- (service.name === "nebula" && !props.showNebulaSettings) ||
- HIDDEN_SERVICES.includes(service.name);
+ (serviceDefinition.name === "nebula" &&
+ !props.showNebulaSettings) ||
+ HIDDEN_SERVICES.includes(serviceDefinition.name);
- const serviceName = getServiceByName(service.name as ServiceName);
+ const serviceName = getServiceByName(
+ serviceDefinition.name as ServiceName,
+ );
const shouldShow = !hidden && serviceName;
if (!shouldShow) {
@@ -575,25 +591,33 @@ function EnabledServicesSetting(props: {
}
let configurationLink: string | undefined;
- if (service.name === "embeddedWallets" && srv.enabled) {
+ if (
+ serviceDefinition.name === "embeddedWallets" &&
+ service.enabled
+ ) {
configurationLink = props.paths.inAppConfig;
- } else if (service.name === "bundler" && srv.enabled) {
+ } else if (
+ serviceDefinition.name === "bundler" &&
+ service.enabled
+ ) {
configurationLink = props.paths.aaConfig;
- } else if (service.name === "pay") {
+ } else if (serviceDefinition.name === "pay" && service.enabled) {
configurationLink = props.paths.payConfig;
}
return (
{/* Left */}
-
{service.title}
+
+ {serviceDefinition.title}
+
- {service.description}
+ {serviceDefinition.description}
@@ -613,36 +637,43 @@ function EnabledServicesSetting(props: {
)}
- {service.actions.length > 0 && (
+ {serviceDefinition.actions.length > 0 && (
- {service.actions.map((sa) => (
-
-
-
-
- handleAction(idx, srv, sa.name, !!checked)
- }
- />
- {sa.title}
-
-
-
- ))}
+ {serviceDefinition.actions.map((sa) => {
+ return (
+
+
+
+
+ toggleServiceAction(
+ idx,
+ service,
+ sa.name,
+ !!checked,
+ )
+ }
+ />
+ {sa.title}
+
+
+
+ );
+ })}
)}
{/* Right */}
- update(idx, {
- ...srv,
+ checked={service.enabled}
+ onCheckedChange={(v) => {
+ return formFields.update(idx, {
+ ...service,
enabled: !!v,
- })
- }
+ });
+ }}
/>
);
@@ -653,15 +684,19 @@ function EnabledServicesSetting(props: {
);
}
-function APIKeyDetails({
- apiKey,
+function ProjectKeyDetails({
+ project,
rotateSecretKey,
}: {
- rotateSecretKey: () => Promise;
- apiKey: ApiKey;
+ rotateSecretKey: RotateSecretKey;
+ project: Project;
}) {
- const { createdAt, updatedAt, lastAccessedAt } = apiKey;
- const [secretKeyMasked, setSecretKeyMasked] = useState(apiKey.secretMasked);
+ // currently only showing the first secret key
+ const { createdAt, updatedAt, lastAccessedAt } = project;
+ const [secretKeyMasked, setSecretKeyMasked] = useState(
+ project.secretKeys[0]?.masked,
+ );
+ const clientId = project.publishableKey;
return (
@@ -672,9 +707,9 @@ function APIKeyDetails({
@@ -706,9 +741,17 @@ function APIKeyDetails({
)}
-
-
-
+
+
+
);
@@ -716,27 +759,32 @@ function APIKeyDetails({
function TimeInfo(props: {
label: string;
- date: string | undefined;
+ date: string | null;
+ fallbackText: string;
}) {
return (
{props.label}
- {props.date ? format(new Date(props.date), "MMMM dd, yyyy") : "Never"}
+ {props.date
+ ? format(new Date(props.date), "MMMM dd, yyyy")
+ : props.fallbackText}
);
}
function DeleteProject(props: {
- id: string;
- name: string;
- deleteMutation: UseMutationResult;
+ projectName: string;
+ deleteProject: DeleteProject;
onDeleteSuccessful: () => void;
}) {
- const { id, name, deleteMutation, onDeleteSuccessful } = props;
const trackEvent = useTrack();
+ const deleteProject = useMutation({
+ mutationFn: props.deleteProject,
+ });
+
const handleRevoke = () => {
trackEvent({
category: "api-keys",
@@ -744,10 +792,10 @@ function DeleteProject(props: {
label: "attempt",
});
- deleteMutation.mutate(id, {
+ deleteProject.mutate(undefined, {
onSuccess: () => {
toast.success("Project deleted successfully");
- onDeleteSuccessful();
+ props.onDeleteSuccessful();
trackEvent({
category: "api-keys",
action: "revoke",
@@ -775,18 +823,18 @@ function DeleteProject(props: {
buttonOnClick={() => handleRevoke()}
buttonLabel="Delete project"
confirmationDialog={{
- title: `Delete project "${name}"?`,
+ title: `Delete project "${props.projectName}"?`,
description: description,
}}
description={description}
- isPending={deleteMutation.isPending}
+ isPending={deleteProject.isPending}
title="Delete Project"
/>
);
}
function RotateSecretKeyButton(props: {
- rotateSecretKey: () => Promise;
+ rotateSecretKey: RotateSecretKey;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
@@ -835,7 +883,7 @@ type RotateSecretKeyScreen =
| { id: "save-newkey"; secretKey: string };
function RotateSecretKeyModalContent(props: {
- rotateSecretKey: () => Promise;
+ rotateSecretKey: RotateSecretKey;
closeModal: () => void;
disableModalClose: () => void;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
@@ -871,7 +919,7 @@ function RotateSecretKeyModalContent(props: {
}
function RotateSecretKeyInitialScreen(props: {
- rotateSecretKey: () => Promise;
+ rotateSecretKey: RotateSecretKey;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
closeModal: () => void;
}) {
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPageForTeams.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPageForTeams.tsx
deleted file mode 100644
index 171a7acdede..00000000000
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPageForTeams.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-"use client";
-
-import type { Team } from "@/api/team";
-import { useDashboardRouter } from "@/lib/DashboardRouter";
-import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
-import { ProjectGeneralSettingsPage } from "./ProjectGeneralSettingsPage";
-
-export function ProjectGeneralSettingsPageForTeams(props: {
- team: Team;
- project_slug: string;
- apiKey: ApiKey;
- projectId: string;
-}) {
- const router = useDashboardRouter();
- const { team, project_slug, apiKey, projectId } = props;
- const projectLayout = `/team/${team.slug}/${project_slug}`;
-
- // TODO - add a Project Image form field on this page
-
- return (
- {
- router.refresh();
- }}
- showNebulaSettings={team.enabledScopes.includes("nebula")}
- />
- );
-}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/page.tsx
index 2427cc96ede..a577312c9ae 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/page.tsx
@@ -1,38 +1,31 @@
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
-import { notFound, redirect } from "next/navigation";
-import { getAPIKeyForProjectId } from "../../../../api/lib/getAPIKeys";
-import { ProjectGeneralSettingsPageForTeams } from "./ProjectGeneralSettingsPageForTeams";
+import { redirect } from "next/navigation";
+import { ProjectGeneralSettingsPage } from "./ProjectGeneralSettingsPage";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const { team_slug, project_slug } = await props.params;
- const team = await getTeamBySlug(team_slug);
+ const [team, project] = await Promise.all([
+ getTeamBySlug(team_slug),
+ getProject(team_slug, project_slug),
+ ]);
if (!team) {
redirect("/team");
}
- const project = await getProject(team_slug, project_slug);
-
if (!project) {
- notFound();
- }
-
- const apiKey = await getAPIKeyForProjectId(project.id);
-
- if (!apiKey) {
- notFound();
+ redirect(`/team/${team_slug}`);
}
return (
-
);
}
diff --git a/apps/dashboard/src/app/team/components/TeamHeader/team-header-logged-in.client.tsx b/apps/dashboard/src/app/team/components/TeamHeader/team-header-logged-in.client.tsx
index 4ffef4b7de2..b3a586894b9 100644
--- a/apps/dashboard/src/app/team/components/TeamHeader/team-header-logged-in.client.tsx
+++ b/apps/dashboard/src/app/team/components/TeamHeader/team-header-logged-in.client.tsx
@@ -8,7 +8,7 @@ import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { useCallback, useState } from "react";
import { useActiveWallet, useDisconnect } from "thirdweb/react";
-import { LazyCreateAPIKeyDialog } from "../../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
+import { LazyCreateProjectDialog } from "../../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { doLogout } from "../../../login/auth-actions";
import {
type TeamHeaderCompProps,
@@ -65,27 +65,26 @@ export function TeamHeaderLoggedIn(props: {
-
- setCreateProjectDialogState({
- isOpen: false,
- })
- }
- onCreateAndComplete={() => {
- // refresh projects
- router.refresh();
- }}
- enableNebulaServiceByDefault={
- createProjectDialogState.isOpen &&
- createProjectDialogState.team.enabledScopes.includes("nebula")
- }
- />
+ {createProjectDialogState.isOpen && (
+
+ setCreateProjectDialogState({
+ isOpen: false,
+ })
+ }
+ onCreateAndComplete={() => {
+ // refresh projects
+ router.refresh();
+ }}
+ enableNebulaServiceByDefault={
+ createProjectDialogState.isOpen &&
+ createProjectDialogState.team.enabledScopes.includes("nebula")
+ }
+ />
+ )}
);
}
diff --git a/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx b/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx
index 4451e49fa2f..a09a1842846 100644
--- a/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx
+++ b/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
-import { accountStub, createApiKeyStub } from "../../../stories/stubs";
+import { projectStub } from "../../../stories/stubs";
import { mobileViewport } from "../../../stories/utils";
import { InAppWalletSettingsUI } from "./index";
@@ -46,8 +46,6 @@ export const FreePlanMobile: Story = {
},
};
-const apiKeyStub = createApiKeyStub();
-
function Variants(props: {
canEditAdvancedFeatures: boolean;
}) {
@@ -56,11 +54,16 @@ function Variants(props: {
{}}
- twAccount={accountStub()}
/>
diff --git a/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx b/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
index 6fd43deeaab..aa2ca66a4d2 100644
--- a/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
+++ b/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
@@ -1,7 +1,10 @@
"use client";
+import type { Project } from "@/api/projects";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Spinner } from "@/components/ui/Spinner/Spinner";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -17,33 +20,29 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
-import {
- type Account,
- type ApiKey,
- type ApiKeyService,
- type UpdateKeyInput,
- useUpdateApiKey,
-} from "@3rdweb-sdk/react/hooks/useApi";
+import { updateProjectClient } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation } from "@tanstack/react-query";
+import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils";
import { GatedSwitch } from "components/settings/Account/Billing/GatedSwitch";
import {
type ApiKeyEmbeddedWalletsValidationSchema,
apiKeyEmbeddedWalletsValidationSchema,
} from "components/settings/ApiKeys/validations";
import { useTrack } from "hooks/analytics/useTrack";
-import { PlusIcon, Trash2Icon } from "lucide-react";
+import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
import type React from "react";
import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { toArrFromList } from "utils/string";
+import type { Team } from "../../../@/api/team";
type InAppWalletSettingsPageProps = {
- apiKey: Pick<
- ApiKey,
- "id" | "name" | "domains" | "bundleIds" | "services" | "redirectUrls"
- >;
trackingCategory: string;
- twAccount: Account;
+ project: Project;
+ teamId: string;
+ teamSlug: string;
+ validTeamPlan: Team["billingPlan"];
};
const TRACKING_CATEGORY = "embedded-wallet";
@@ -55,12 +54,23 @@ type UpdateAPIKeyTrackingData = {
};
export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
- const mutation = useUpdateApiKey();
+ const updateProject = useMutation({
+ mutationFn: async (projectValues: Partial
) => {
+ await updateProjectClient(
+ {
+ projectId: props.project.id,
+ teamId: props.teamId,
+ },
+ projectValues,
+ );
+ },
+ });
+
const { trackingCategory } = props;
const trackEvent = useTrack();
- function handleAPIKeyUpdate(
- newValue: UpdateKeyInput,
+ function handleUpdateProject(
+ projectValues: Partial,
trackingData: UpdateAPIKeyTrackingData,
) {
trackEvent({
@@ -69,7 +79,7 @@ export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
label: "attempt",
});
- mutation.mutate(newValue, {
+ updateProject.mutate(projectValues, {
onSuccess: () => {
toast.success("In-App Wallet API Key configuration updated");
trackEvent({
@@ -93,50 +103,88 @@ export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
}
return (
-
);
}
-export const InAppWalletSettingsUI: React.FC<
+const InAppWalletSettingsPageUI: React.FC<
InAppWalletSettingsPageProps & {
canEditAdvancedFeatures: boolean;
updateApiKey: (
- apiKey: UpdateKeyInput,
+ projectValues: Partial,
trackingData: UpdateAPIKeyTrackingData,
) => void;
isUpdating: boolean;
}
> = (props) => {
- const { canEditAdvancedFeatures, apiKey } = props;
- const services: ApiKeyService[] = apiKey.services || [];
+ const embeddedWalletService = props.project.services.find(
+ (service) => service.name === "embeddedWallets",
+ );
+
+ if (!embeddedWalletService) {
+ return (
+
+
+ In-App wallets service is disabled
+
+ Enable In-App wallets service in the{" "}
+
+ project settings
+ {" "}
+ to configure settings
+
+
+ );
+ }
- const serviceIdx = services.findIndex(
- (srv) => srv.name === "embeddedWallets",
+ return (
+
);
- const config: ApiKeyService | undefined = services[serviceIdx];
+};
+
+export const InAppWalletSettingsUI: React.FC<
+ Omit & {
+ canEditAdvancedFeatures: boolean;
+ updateApiKey: (
+ projectValues: Partial,
+ trackingData: UpdateAPIKeyTrackingData,
+ ) => void;
+ isUpdating: boolean;
+ embeddedWalletService: ProjectEmbeddedWalletsService;
+ }
+> = (props) => {
+ const { canEditAdvancedFeatures } = props;
+ const services = props.project.services;
+
+ const config = props.embeddedWalletService;
const hasCustomBranding =
- !!config?.applicationImageUrl?.length || !!config?.applicationName?.length;
+ !!config.applicationImageUrl?.length || !!config.applicationName?.length;
const form = useForm({
resolver: zodResolver(apiKeyEmbeddedWalletsValidationSchema),
values: {
- customAuthEndpoint: config?.customAuthEndpoint,
- customAuthentication: config?.customAuthentication,
+ customAuthEndpoint: config.customAuthEndpoint || undefined,
+ customAuthentication: config.customAuthentication || undefined,
...(hasCustomBranding
? {
branding: {
- applicationName: config.applicationName,
- applicationImageUrl: config.applicationImageUrl,
+ applicationName: config.applicationName || undefined,
+ applicationImageUrl: config.applicationImageUrl || undefined,
},
}
: undefined),
- redirectUrls: apiKey.redirectUrls.join("\n"),
+ redirectUrls: (config.redirectUrls || []).join("\n"),
},
});
@@ -168,29 +216,23 @@ export const InAppWalletSettingsUI: React.FC<
);
}
- const { id, name, domains, bundleIds } = apiKey;
-
- // FIXME: This must match components/settings/ApiKeys/Edit/index.tsx
- // Make it more generic w/o me thinking of values
- const newServices = [...services];
+ const newServices = services.map((service) => {
+ if (service.name !== "embeddedWallets") {
+ return service;
+ }
- if (services[serviceIdx]) {
- newServices[serviceIdx] = {
- ...services[serviceIdx],
+ return {
+ ...service,
customAuthentication,
customAuthEndpoint,
applicationImageUrl: branding?.applicationImageUrl,
- applicationName: branding?.applicationName || apiKey.name,
+ applicationName: branding?.applicationName || props.project.name,
+ redirectUrls: toArrFromList(redirectUrls || "", true),
};
- }
+ });
props.updateApiKey(
{
- id,
- name,
- domains,
- bundleIds,
- redirectUrls: toArrFromList(redirectUrls || "", true),
services: newServices,
},
{
diff --git a/apps/dashboard/src/components/pay/PayConfig.tsx b/apps/dashboard/src/components/pay/PayConfig.tsx
index f1b70c65ed7..7696fdf287c 100644
--- a/apps/dashboard/src/components/pay/PayConfig.tsx
+++ b/apps/dashboard/src/components/pay/PayConfig.tsx
@@ -1,6 +1,9 @@
"use client";
+import type { Project } from "@/api/projects";
import { SettingsCard } from "@/components/blocks/SettingsCard";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
@@ -9,34 +12,31 @@ import {
FormLabel,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { type ApiKey, useUpdateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
+import { updateProjectClient } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation } from "@tanstack/react-query";
import {
type ApiKeyPayConfigValidationSchema,
apiKeyPayConfigValidationSchema,
} from "components/settings/ApiKeys/validations";
import { useTrack } from "hooks/analytics/useTrack";
+import { CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
interface PayConfigProps {
- apiKey: Pick<
- ApiKey,
- | "services"
- | "id"
- | "name"
- | "domains"
- | "bundleIds"
- | "services"
- | "redirectUrls"
- >;
+ project: Project;
+ teamId: string;
+ teamSlug: string;
}
const TRACKING_CATEGORY = "pay";
-export const PayConfig: React.FC = ({ apiKey }) => {
- const payService = apiKey.services?.find((service) => service.name === "pay");
+export const PayConfig: React.FC = (props) => {
+ const payService = props.project.services.find(
+ (service) => service.name === "pay",
+ );
const form = useForm({
resolver: zodResolver(apiKeyPayConfigValidationSchema),
@@ -47,13 +47,20 @@ export const PayConfig: React.FC = ({ apiKey }) => {
const trackEvent = useTrack();
- const mutation = useUpdateApiKey();
+ const updateProject = useMutation({
+ mutationFn: async (projectValues: Partial) => {
+ await updateProjectClient(
+ {
+ projectId: props.project.id,
+ teamId: props.teamId,
+ },
+ projectValues,
+ );
+ },
+ });
const handleSubmit = form.handleSubmit(({ payoutAddress }) => {
- const services = apiKey.services;
- if (!services) {
- throw new Error("Bad state: Missing services");
- }
+ const services = props.project.services;
const newServices = services.map((service) => {
if (service.name !== "pay") {
@@ -66,40 +73,54 @@ export const PayConfig: React.FC = ({ apiKey }) => {
};
});
- const formattedValues = {
- ...apiKey,
- services: newServices,
- };
-
- const mutationPromise = mutation.mutateAsync(formattedValues, {
- onSuccess: () => {
- trackEvent({
- category: TRACKING_CATEGORY,
- action: "configuration-update",
- label: "success",
- data: {
- payoutAddress,
- },
- });
+ updateProject.mutate(
+ {
+ services: newServices,
},
- onError: (err) => {
- trackEvent({
- category: TRACKING_CATEGORY,
- action: "configuration-update",
- label: "error",
- error: err,
- });
+ {
+ onSuccess: () => {
+ toast.success("Fee sharing updated");
+ trackEvent({
+ category: TRACKING_CATEGORY,
+ action: "configuration-update",
+ label: "success",
+ data: {
+ payoutAddress,
+ },
+ });
+ },
+ onError: (err) => {
+ toast.error("Failed to update fee sharing");
+ console.error(err);
+ trackEvent({
+ category: TRACKING_CATEGORY,
+ action: "configuration-update",
+ label: "error",
+ error: err,
+ });
+ },
},
- });
-
- toast.promise(mutationPromise, {
- success: "Changes saved",
- error: (err) => {
- return `Failed to save changes: ${err.message}`;
- },
- });
+ );
});
+ if (!payService) {
+ return (
+
+
+ Pay service is disabled
+
+ Enable Pay service in{" "}
+
+ project settings
+ {" "}
+ to configure settings
+
+
+ );
+ }
+
return (