diff --git a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx
index 5db6a5eb906..8498dfb2bb9 100644
--- a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx
+++ b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx
@@ -28,7 +28,7 @@ export const Mobile: Story = {
function Story() {
return (
-
+
;
+
+export function UnderlineLink(props: LinkProps) {
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/@/lib/DashboardRouter.tsx b/apps/dashboard/src/@/lib/DashboardRouter.tsx
index ac1fc432666..971a02def37 100644
--- a/apps/dashboard/src/@/lib/DashboardRouter.tsx
+++ b/apps/dashboard/src/@/lib/DashboardRouter.tsx
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
const width = isLoading ? progress : 100;
return (
fetchDashboardContractMetadata(contract),
+ refetchOnWindowFocus: false,
});
}
diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts
deleted file mode 100644
index c1b3f7636bd..00000000000
--- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { addContractToMultiChainRegistry } from "components/contract-components/utils";
-import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
-import {
- DASHBOARD_ENGINE_RELAYER_URL,
- DASHBOARD_FORWARDER_ADDRESS,
-} from "constants/misc";
-import type { BasicContract } from "contract-ui/types/types";
-import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
-import { useAllChainsData } from "hooks/chains/allChains";
-import { useMemo } from "react";
-import { sendAndConfirmTransaction } from "thirdweb";
-import { remove } from "thirdweb/extensions/thirdweb";
-import { useActiveAccount } from "thirdweb/react";
-import invariant from "tiny-invariant";
-
-export function useMultiChainRegContractList(walletAddress?: string) {
- return useQuery({
- queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
- queryFn: async () => {
- invariant(walletAddress, "walletAddress is required");
- const contracts = await getAllMultichainRegistry({
- contract: MULTICHAIN_REGISTRY_CONTRACT,
- address: walletAddress,
- });
-
- return contracts;
- },
-
- enabled: !!walletAddress,
- });
-}
-
-interface Options {
- onlyMainnet?: boolean;
-}
-
-export const useAllContractList = (
- walletAddress: string | undefined,
- { onlyMainnet }: Options = { onlyMainnet: false },
-) => {
- const multiChainQuery = useMultiChainRegContractList(walletAddress);
-
- // TODO - instead of using ALL chains, fetch only the ones used here
- const { idToChain } = useAllChainsData();
- const contractList = useMemo(() => {
- const data = multiChainQuery.data || [];
-
- const mainnets: BasicContract[] = [];
- const testnets: BasicContract[] = [];
-
- // biome-ignore lint/complexity/noForEach: FIXME
- data.forEach((net) => {
- const chn = idToChain.get(net.chainId);
- if (chn && chn.status !== "deprecated") {
- if (chn.testnet) {
- testnets.push(net);
- } else {
- mainnets.push(net);
- }
- }
- });
-
- mainnets.sort((a, b) => a.chainId - b.chainId);
-
- if (onlyMainnet) {
- return mainnets;
- }
-
- testnets.sort((a, b) => a.chainId - b.chainId);
- return mainnets.concat(testnets);
- }, [multiChainQuery.data, onlyMainnet, idToChain]);
-
- return {
- ...multiChainQuery,
- data: contractList,
- };
-};
-
-type RemoveContractParams = {
- contractAddress: string;
- chainId: number;
-};
-
-export function useRemoveContractMutation() {
- const queryClient = useQueryClient();
- const account = useActiveAccount();
- return useMutation({
- mutationFn: async (data: RemoveContractParams) => {
- invariant(data.chainId, "chainId not provided");
- invariant(account, "No wallet connected");
- const { contractAddress, chainId } = data;
- const transaction = remove({
- contract: MULTICHAIN_REGISTRY_CONTRACT,
- deployer: account.address,
- deployment: contractAddress,
- chainId: BigInt(chainId),
- });
- return await sendAndConfirmTransaction({
- transaction,
- account,
- gasless: {
- experimentalChainlessSupport: true,
- provider: "engine",
- relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
- relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
- },
- });
- },
-
- onSettled: () => {
- return queryClient.invalidateQueries({
- queryKey: ["dashboard-registry"],
- });
- },
- });
-}
-
-type AddContractParams = {
- contractAddress: string;
- chainId: number;
-};
-
-export function useAddContractMutation() {
- const account = useActiveAccount();
-
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (data: AddContractParams) => {
- invariant(account, "cannot add a contract without an address");
-
- return await addContractToMultiChainRegistry(
- {
- address: data.contractAddress,
- chainId: data.chainId,
- },
- account,
- 300000n,
- );
- },
-
- onSettled: () => {
- return queryClient.invalidateQueries({
- queryKey: ["dashboard-registry"],
- });
- },
- });
-}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.client.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.client.tsx
index 23f5d0226b7..41b70324b3d 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.client.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.client.tsx
@@ -1,8 +1,9 @@
"use client";
import { useQuery } from "@tanstack/react-query";
-import type { ThirdwebContract } from "thirdweb";
+import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
+import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
@@ -13,6 +14,8 @@ export function ContractPageLayoutClient(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
+ teamsAndProjects: MinimalTeamsAndProjects | undefined;
+ client: ThirdwebClient;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.tsx
index 10565dcf70c..ef2a5b942f8 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.tsx
@@ -3,8 +3,9 @@ import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
-import type { ThirdwebContract } from "thirdweb";
+import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
+import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
@@ -14,6 +15,8 @@ export function ContractPageLayout(props: {
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
+ client: ThirdwebClient;
+ teamsAndProjects: MinimalTeamsAndProjects | undefined;
externalLinks:
| {
name: string;
@@ -27,6 +30,8 @@ export function ContractPageLayout(props: {
sidebarLinks,
dashboardContractMetadata,
externalLinks,
+ teamsAndProjects,
+ client,
} = props;
return (
@@ -49,6 +54,8 @@ export function ContractPageLayout(props: {
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
+ teamsAndProjects={teamsAndProjects}
+ client={client}
/>
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx
index fd8cbcec2f4..b83588d0dcf 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx
@@ -1,19 +1,31 @@
"use client";
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 type { EVMContractInfo } from "@3rdweb-sdk/react";
import {
- useAddContractMutation,
- useAllContractList,
-} from "@3rdweb-sdk/react/hooks/useRegistry";
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import type { EVMContractInfo } from "@3rdweb-sdk/react";
import { useTrack } from "hooks/analytics/useTrack";
-import { useTxNotifications } from "hooks/useTxNotifications";
import { CodeIcon, PlusIcon } from "lucide-react";
+import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import type { Chain } from "thirdweb";
-import { useActiveAccount } from "thirdweb/react";
+import { useState } from "react";
+import { toast } from "sonner";
+import type { Chain, ThirdwebClient } from "thirdweb";
+import {
+ AddToProjectSelector,
+ type MinimalTeamsAndProjects,
+} from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
+import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
@@ -22,6 +34,8 @@ type AddToDashboardCardProps = {
chain: Chain;
contractInfo: EVMContractInfo;
hideCodePageLink?: boolean;
+ teamsAndProjects: MinimalTeamsAndProjects | undefined;
+ client: ThirdwebClient;
};
export const PrimaryDashboardButton: React.FC
= ({
@@ -29,40 +43,13 @@ export const PrimaryDashboardButton: React.FC = ({
chain,
contractInfo,
hideCodePageLink,
+ teamsAndProjects,
+ client,
}) => {
- const addContract = useAddContractMutation();
- const trackEvent = useTrack();
- const walletAddress = useActiveAccount()?.address;
const pathname = usePathname();
- const registry = useAllContractList(walletAddress);
- const { onSuccess: onAddSuccess, onError: onAddError } = useTxNotifications(
- "Successfully imported",
- "Failed to import",
- );
-
- const isInRegistry =
- registry.isFetched &&
- registry.data?.find(
- (c) =>
- contractAddress &&
- // compare address...
- c.address.toLowerCase() === contractAddress.toLowerCase() &&
- // ... and chainId
- c.chainId === chain.id,
- ) &&
- registry.isSuccess;
-
- if (
- !walletAddress ||
- !contractAddress ||
- !chain ||
- pathname?.includes("payments")
- ) {
- return null;
- }
-
- if (isInRegistry) {
+ // if user is not logged in
+ if (!teamsAndProjects) {
if (hideCodePageLink) {
return null;
}
@@ -84,54 +71,163 @@ export const PrimaryDashboardButton: React.FC = ({
}
return (
-
+
);
};
+
+function AddToProjectButton(props: {
+ contractAddress: string;
+ teamsAndProjects: MinimalTeamsAndProjects;
+ chainId: string;
+ client: ThirdwebClient;
+}) {
+ return (
+
+ );
+}
+
+function AddToProjectModalContent(props: {
+ teamsAndProjects: MinimalTeamsAndProjects;
+ chainId: string;
+ contractAddress: string;
+ client: ThirdwebClient;
+}) {
+ const trackEvent = useTrack();
+ const addContractToProject = useAddContractToProject();
+
+ const [importSelection, setImportSelection] = useState({
+ team: props.teamsAndProjects[0]?.team,
+ project: props.teamsAndProjects[0]?.projects[0],
+ });
+
+ const selectedTeam = props.teamsAndProjects.find(
+ (t) => t.team.id === importSelection.team?.id,
+ );
+
+ function handleImport(params: {
+ teamId: string;
+ projectId: string;
+ }) {
+ trackEvent({
+ category: TRACKING_CATEGORY,
+ action: "add-to-dashboard",
+ label: "attempt",
+ contractAddress: props.contractAddress,
+ });
+ addContractToProject.mutate(
+ {
+ contractAddress: props.contractAddress,
+ teamId: params.teamId,
+ projectId: params.projectId,
+ chainId: props.chainId,
+ },
+ {
+ onSuccess: () => {
+ toast.success("Contract added to the project successfully");
+ trackEvent({
+ category: TRACKING_CATEGORY,
+ action: "add-to-dashboard",
+ label: "success",
+ contractAddress: props.contractAddress,
+ });
+ },
+ onError: (err) => {
+ if (err.message.includes("PROJECT_CONTRACT_ALREADY_EXISTS")) {
+ toast.error("Contract is already added to the project");
+ } else {
+ toast.error("Failed to import contract");
+ }
+ trackEvent({
+ category: TRACKING_CATEGORY,
+ action: "add-to-dashboard",
+ label: "error",
+ contractAddress: props.contractAddress,
+ error: err,
+ });
+ },
+ },
+ );
+ }
+
+ const isImportEnabled = !!importSelection.project && !!importSelection.team;
+
+ return (
+
+
+
+
+ {/* No projects alert */}
+ {selectedTeam?.projects.length === 0 && (
+
+
+ Selected team has no projects
+
+
+ Create Project
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
index 97c9ee710b3..60a7edc66ca 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
@@ -1,8 +1,12 @@
+import { getProjects } from "@/api/projects";
+import { getTeams } from "@/api/team";
+import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
+import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
@@ -38,11 +42,16 @@ export default async function Layout(props: {
notFound();
}
+ const client = getThirdwebClient();
+ const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
+
if (contract.chain.id === localhost.id) {
return (
{props.children}
@@ -73,12 +82,43 @@ export default async function Layout(props: {
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
+ teamsAndProjects={teamsAndProjects}
+ client={client}
>
{props.children}
);
}
+async function getTeamsAndProjectsIfLoggedIn() {
+ try {
+ const teams = await getTeams();
+
+ if (!teams) {
+ return undefined;
+ }
+
+ const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
+ teams.map(async (team) => ({
+ team: {
+ id: team.id,
+ name: team.name,
+ slug: team.slug,
+ image: team.image,
+ },
+ projects: (await getProjects(team.slug)).map((x) => ({
+ id: x.id,
+ name: x.name,
+ })),
+ })),
+ );
+
+ return teamsAndProjects;
+ } catch {
+ return undefined;
+ }
+}
+
export async function generateMetadata(props: {
params: Promise<{
contractAddress: string;
diff --git a/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/ProfileUI.tsx b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/ProfileUI.tsx
index e76688122de..ae14e98da8b 100644
--- a/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/ProfileUI.tsx
+++ b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/ProfileUI.tsx
@@ -1,8 +1,6 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { fetchPublishedContracts } from "components/contract-components/fetchPublishedContracts";
-import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
import { Suspense } from "react";
-import { getSortedDeployedContracts } from "../../../account/contracts/_components/getSortedDeployedContracts";
import { ProfileHeader } from "./components/profile-header";
import { PublishedContracts } from "./components/published-contracts";
@@ -32,36 +30,10 @@ export function ProfileUI(props: {