diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx new file mode 100644 index 00000000000..289ef906956 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx @@ -0,0 +1,19 @@ +import { redirect } from "next/navigation"; +import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken"; +import { DeployedContractsPage } from "../../../../team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage"; + +export default function Page() { + const accountAddress = getAuthTokenWalletAddress(); + if (!accountAddress) { + return redirect( + `/login?next=${encodeURIComponent("/dashboard/contracts/deploy")}`, + ); + } + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx new file mode 100644 index 00000000000..27dcd951ac8 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx @@ -0,0 +1,13 @@ +import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert"; +import { ContractsSidebarLayout } from "../../../../core-ui/sidebar/contracts"; + +export default function Layout(props: { + children: React.ReactNode; +}) { + return ( + + + {props.children} + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/layout.tsx b/apps/dashboard/src/app/(dashboard)/layout.tsx index 3b08e193fc2..c89aefb25ac 100644 --- a/apps/dashboard/src/app/(dashboard)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/layout.tsx @@ -7,7 +7,7 @@ export default function DashboardLayout(props: { children: React.ReactNode }) {
-
{props.children}
+
{props.children}
diff --git a/apps/dashboard/src/app/api/lib/getAuthToken.ts b/apps/dashboard/src/app/api/lib/getAuthToken.ts index 36c4b4232a2..24f3174458d 100644 --- a/apps/dashboard/src/app/api/lib/getAuthToken.ts +++ b/apps/dashboard/src/app/api/lib/getAuthToken.ts @@ -10,3 +10,19 @@ export function getAuthToken() { return token; } + +export function getAuthTokenWalletAddress() { + const cookiesManager = cookies(); + const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value; + if (!activeAccount) { + return null; + } + + const token = cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value; + + if (token) { + return activeAccount; + } + + return null; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx new file mode 100644 index 00000000000..7949da8aa89 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { DownloadIcon, PlusIcon } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { Button } from "../../../../../@/components/ui/button"; +import { ImportModal } from "../../../../../components/contract-components/import-contract/modal"; + +export function DeployedContractsPageHeader() { + const [importModalOpen, setImportModalOpen] = useState(false); + + return ( +
+ { + setImportModalOpen(false); + }} + /> +
+
+

+ Your contracts +

+

+ The list of contract instances that you have deployed or imported + with thirdweb across all networks +

+
+
+ + +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx new file mode 100644 index 00000000000..f4d559c9995 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx @@ -0,0 +1,48 @@ +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Suspense } from "react"; +import { ClientOnly } from "../../../../../../components/ClientOnly/ClientOnly"; +import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader"; +import { DeployedContractsTable } from "./DeployedContractsTable"; +import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy"; +import { getSortedDeployedContracts } from "./getSortedDeployedContracts"; + +export function DeployedContractsPage(props: { + address: string; + className?: string; +}) { + return ( +
+ +
+ }> + + +
+ ); +} + +async function DeployedContractsPageAsync(props: { + address: string; +}) { + const deployedContracts = await getSortedDeployedContracts({ + address: props.address, + }); + + if (deployedContracts.length === 0) { + return ; + } + + return ( + }> + + + ); +} + +function Loading() { + return ( +
+ +
+ ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx new file mode 100644 index 00000000000..8e69d6220fc --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx @@ -0,0 +1,18 @@ +"use client"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import type { BasicContract } from "contract-ui/types/types"; +import { DeployedContracts } from "../../../../../../components/contract-components/tables/deployed-contracts"; + +export function DeployedContractsTable(props: { + contracts: BasicContract[]; +}) { + const router = useDashboardRouter(); + return ( + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx index b16212281f9..29ee0c5474c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx @@ -1,26 +1,15 @@ "use client"; import { TabButtons } from "@/components/ui/tabs"; import { useDashboardRouter } from "@/lib/DashboardRouter"; -import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet"; import Image from "next/image"; import { useMemo, useState } from "react"; -import { useActiveAccount } from "thirdweb/react"; import { ImportModal } from "../../../../../../components/contract-components/import-contract/modal"; import { StepsCard } from "../../../../../../components/dashboard/StepsCard"; import { useTrack } from "../../../../../../hooks/analytics/useTrack"; export function GetStartedWithContractsDeploy() { - const address = useActiveAccount()?.address; const steps = useMemo( () => [ - { - title: "Connect your wallet to get started", - description: - "In order to interact with your contracts you need to connect an EVM compatible wallet.", - children: , - completed: !!address, - }, - { title: "Build, deploy or import a contract", description: @@ -29,7 +18,7 @@ export function GetStartedWithContractsDeploy() { completed: false, // because we only show this component if the user does not have any contracts }, ], - [address], + [], ); return ( @@ -139,7 +128,7 @@ const DeployOptions = () => {

{activeTabContent.title}

-

+

{activeTabContent.description}

diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx new file mode 100644 index 00000000000..f7395fd0ee0 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx @@ -0,0 +1,42 @@ +import type { BasicContract } from "contract-ui/types/types"; +import { MULTICHAIN_REGISTRY_CONTRACT } from "../../../../../../constants/contracts"; +import { getAllMultichainRegistry } from "../../../../../../dashboard-extensions/common/read/getAllMultichainRegistry"; +import { fetchChain } from "../../../../../../utils/fetchChain"; + +export async function getSortedDeployedContracts(params: { + address: string; +}) { + const contracts = await getAllMultichainRegistry({ + contract: MULTICHAIN_REGISTRY_CONTRACT, + address: params.address, + }); + + const chainIds = Array.from(new Set(contracts.map((c) => c.chainId))); + const chains = ( + await Promise.allSettled( + chainIds.map((chainId) => fetchChain(chainId.toString())), + ) + ) + .filter((c) => c.status === "fulfilled") + .map((c) => c.value) + .filter((c) => c !== null); + + const mainnetContracts: BasicContract[] = []; + const testnetContracts: BasicContract[] = []; + + for (const contract of contracts) { + const chain = chains.find((chain) => contract.chainId === chain.chainId); + if (chain && chain.status !== "deprecated") { + if (chain.testnet) { + testnetContracts.push(contract); + } else { + mainnetContracts.push(contract); + } + } + } + + mainnetContracts.sort((a, b) => a.chainId - b.chainId); + testnetContracts.sort((a, b) => a.chainId - b.chainId); + + return [...mainnetContracts, ...testnetContracts]; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx index 38c1be3122f..500764ffd23 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx @@ -1,5 +1,5 @@ export default function Layout(props: { children: React.ReactNode; }) { - return
{props.children}
; + return
{props.children}
; } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx index 4389c0b266b..c305fbeae6c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx @@ -1,31 +1,23 @@ -"use client"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { useAllContractList } from "@3rdweb-sdk/react/hooks/useRegistry"; -import { useActiveAccount } from "thirdweb/react"; -import { DeployedContracts } from "../../../../../components/contract-components/tables/deployed-contracts"; -import { GetStartedWithContractsDeploy } from "./_components/GetStartedWithContractsDeploy"; +import { redirect } from "next/navigation"; +import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken"; +import { DeployedContractsPage } from "./_components/DeployedContractsPage"; -export default function Page() { - const address = useActiveAccount()?.address; - const deployedContracts = useAllContractList(address); - const hasContracts = - deployedContracts.data && deployedContracts.data?.length > 0; +export default function Page(props: { + params: { team_slug: string; project_slug: string }; +}) { + const { team_slug, project_slug } = props.params; + const accountAddress = getAuthTokenWalletAddress(); - if (deployedContracts.isPending) { - return ( -
- -
+ if (!accountAddress) { + return redirect( + `/login?next=${encodeURIComponent(`/team/${team_slug}/${project_slug}/contracts`)}`, ); } return ( -
- {!hasContracts ? ( - - ) : ( - - )} -
+ ); } diff --git a/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx b/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx index 5cdb3d9c95a..331f94760e8 100644 --- a/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx +++ b/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx @@ -9,6 +9,7 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { SkeletonContainer } from "@/components/ui/skeleton"; import { Table, TableBody, @@ -26,13 +27,7 @@ import { import { ChainIcon } from "components/icons/ChainIcon"; import { NetworkSelectDropdown } from "components/selects/NetworkSelectDropdown"; import type { BasicContract } from "contract-ui/types/types"; -import { - DownloadIcon, - EllipsisVerticalIcon, - PlusIcon, - XIcon, -} from "lucide-react"; -import Link from "next/link"; +import { EllipsisVerticalIcon, XIcon } from "lucide-react"; import { memo, useEffect, useMemo, useState } from "react"; import { type Column, @@ -44,80 +39,42 @@ import { } from "react-table"; import { useAllChainsData } from "../../../hooks/chains/allChains"; import { useChainSlug } from "../../../hooks/chains/chainSlug"; -import { ImportModal } from "../import-contract/modal"; import { AsyncContractNameCell, AsyncContractTypeCell } from "./cells"; import { ShowMoreButton } from "./show-more-button"; interface DeployedContractsProps { - noHeader?: boolean; - contractListQuery: ReturnType; + contractList: ReturnType["data"]; + isPending: boolean; limit?: number; + onContractRemoved?: () => void; } export const DeployedContracts: React.FC = ({ - noHeader, - contractListQuery, + contractList, limit = 10, + isPending, + onContractRemoved, }) => { - const [importModalOpen, setImportModalOpen] = useState(false); - const chainIdsWithDeployments = useMemo(() => { const set = new Set(); // biome-ignore lint/complexity/noForEach: FIXME - contractListQuery.data.forEach((contract) => { + contractList.forEach((contract) => { set.add(contract.chainId); }); return [...set]; - }, [contractListQuery.data]); + }, [contractList]); return (
- {!noHeader && ( - <> - { - setImportModalOpen(false); - }} - /> -
-
-

- Your contracts -

-

- The list of contract instances that you have deployed or - imported with thirdweb across all networks -

-
-
- - -
-
- - )} - - {contractListQuery.data.length === 0 && contractListQuery.isFetched && ( + {contractList.length === 0 && !isPending && (
No contracts found
@@ -129,11 +86,13 @@ export const DeployedContracts: React.FC = ({ type RemoveFromDashboardButtonProps = { chainId: number; contractAddress: string; + onContractRemoved?: () => void; }; const RemoveFromDashboardButton: React.FC = ({ chainId, contractAddress, + onContractRemoved, }) => { const mutation = useRemoveContractMutation(); @@ -142,7 +101,14 @@ const RemoveFromDashboardButton: React.FC = ({ variant="ghost" onClick={(e) => { e.stopPropagation(); - mutation.mutate({ chainId, contractAddress }); + mutation.mutateAsync( + { chainId, contractAddress }, + { + onSuccess: () => { + onContractRemoved?.(); + }, + }, + ); }} disabled={mutation.isPending} className="!bg-background hover:!bg-accent gap-2" @@ -188,6 +154,7 @@ interface ContractTableProps { limit: number; chainIdsWithDeployments: number[]; loading: boolean; + onContractRemoved?: () => void; } const ContractTable: React.FC = ({ @@ -196,6 +163,7 @@ const ContractTable: React.FC = ({ limit, chainIdsWithDeployments, loading, + onContractRemoved, }) => { const { idToChain } = useAllChainsData(); @@ -236,9 +204,14 @@ const ContractTable: React.FC = ({ return (
-

- {cleanedChainName} -

+ { + return

{v}

; + }} + /> + {data?.testnet && ( Testnet @@ -285,6 +258,7 @@ const ContractTable: React.FC = ({ @@ -292,7 +266,7 @@ const ContractTable: React.FC = ({ }, }, ], - [chainIdsWithDeployments, idToChain], + [chainIdsWithDeployments, idToChain, onContractRemoved], ); const defaultColumn = useMemo( @@ -395,10 +369,12 @@ const ContractTable: React.FC = ({ const ContractTableRow = memo(({ row }: { row: Row }) => { const chainSlug = useChainSlug(row.original.chainId); const router = useDashboardRouter(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...rowProps } = row.getRowProps(); return ( { - const address = useActiveAccount()?.address; - const deployedContracts = useAllContractList(address); - - if (deployedContracts.isPending) { - return ( -
- -
- ); - } - - const hasContracts = - deployedContracts.data && deployedContracts.data?.length > 0; - - if (!hasContracts) { - return ; - } - - return ; -}; - -Contracts.getLayout = (page, props) => ( - - {page} - -); -Contracts.pageId = PageId.Contracts; - -export default Contracts; diff --git a/apps/dashboard/src/pages/profile/[profileAddress].tsx b/apps/dashboard/src/pages/profile/[profileAddress].tsx index 14f43a956c9..a2408cfaf50 100644 --- a/apps/dashboard/src/pages/profile/[profileAddress].tsx +++ b/apps/dashboard/src/pages/profile/[profileAddress].tsx @@ -177,8 +177,8 @@ const UserPage: ThirdwebNextPage = (props: UserPageProps) => { {ens.data?.address && ( )}