From 92e55c8b3dada0ecb36839c6d52a5e6f094e5357 Mon Sep 17 00:00:00 2001 From: GWSzeto Date: Thu, 14 Nov 2024 18:08:54 +0000 Subject: [PATCH] [Dashboard] Feature: Updated Claimable (#5413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tokenId and setToken introduced at the parent level ClaimableModule - Claim Conditions are fetched at the parent level https://linear.app/thirdweb/issue/DES-251/claimable-erc721-erc1155-erc20 --- ## PR-Codex overview This PR focuses on enhancing the `ClaimableModule` functionality by refining claim conditions, improving token ID handling, and updating UI components for better user experience. ### Detailed summary - Updated claim condition checks for `ClaimableModule`. - Added `tokenId` state management in `ClaimableModule`. - Modified claim condition logic to handle ERC721 and ERC1155 tokens. - Enhanced UI with a new checkbox for "No Claim Condition Set". - Improved currency selection logic in `CurrencySelector`. - Refined `pricePerUnit` handling in `claimable.stories.tsx`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../modules/components/Claimable.tsx | 246 +++++++++--------- .../modules/components/CurrencySelector.tsx | 3 +- .../modules/components/claimable.stories.tsx | 36 ++- .../modules/components/module-instance.tsx | 2 +- 4 files changed, 153 insertions(+), 134 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index e7ab3cb3a05..abf3e976214 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -1,4 +1,5 @@ "use client"; + import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange"; import { @@ -7,7 +8,7 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { Alert, AlertTitle } from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Form, @@ -18,19 +19,26 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; import { TransactionButton } from "components/buttons/TransactionButton"; import { addDays, fromUnixTime } from "date-fns"; import { useAllChainsData } from "hooks/chains/allChains"; import { useTxNotifications } from "hooks/useTxNotifications"; import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react"; -import { useCallback } from "react"; +import { + type Dispatch, + type SetStateAction, + useCallback, + useState, +} from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { + NATIVE_TOKEN_ADDRESS, type PreparedTransaction, ZERO_ADDRESS, getContract, @@ -57,22 +65,15 @@ export type ClaimConditionValue = { auxData: string; }; -type ClaimCondition = { - availableSupply: bigint; - allowlistMerkleRoot: `0x${string}`; - pricePerUnit: bigint; - currency: string; - maxMintPerWallet: bigint; - startTimestamp: number; - endTimestamp: number; - auxData: string; -}; +const positiveIntegerRegex = /^[0-9]\d*$/; function ClaimableModule(props: ModuleInstanceProps) { const { contract, ownerAccount } = props; const account = useActiveAccount(); + const [tokenId, setTokenId] = useState(""); const isErc721 = props.contractInfo.name === "ClaimableERC721"; + const isValidTokenId = positiveIntegerRegex.test(tokenId); const primarySaleRecipientQuery = useReadContract( isErc721 ? ClaimableERC721.getSaleConfig : ClaimableERC1155.getSaleConfig, @@ -82,20 +83,27 @@ function ClaimableModule(props: ModuleInstanceProps) { ); const claimConditionQuery = useReadContract( - ClaimableERC721.getClaimCondition, + isErc721 + ? ClaimableERC721.getClaimCondition + : ClaimableERC1155.getClaimCondition, { + tokenId: positiveIntegerRegex.test(tokenId) ? BigInt(tokenId) : 0n, contract: contract, queryOptions: { - enabled: isErc721, + enabled: isErc721 || (!!tokenId && isValidTokenId), }, }, ); - const getClaimConditionErc1155 = (tokenId: string) => - ClaimableERC1155.getClaimCondition({ - contract: contract, - tokenId: BigInt(tokenId), - }); + const noClaimConditionSet = + claimConditionQuery.data?.availableSupply === 0n && + claimConditionQuery.data?.allowlistMerkleRoot === + "0x0000000000000000000000000000000000000000000000000000000000000000" && + claimConditionQuery.data?.pricePerUnit === 0n && + claimConditionQuery.data?.currency === ZERO_ADDRESS && + claimConditionQuery.data?.maxMintPerWallet === 0n && + claimConditionQuery.data?.startTimestamp === 0 && + claimConditionQuery.data?.endTimestamp === 0; const currencyContract = getContract({ address: claimConditionQuery.data?.currency || "", @@ -104,7 +112,6 @@ function ClaimableModule(props: ModuleInstanceProps) { }); const shouldFetchTokenDecimals = - isErc721 && claimConditionQuery.data && claimConditionQuery.data?.currency !== ZERO_ADDRESS; @@ -217,10 +224,8 @@ function ClaimableModule(props: ModuleInstanceProps) { }} claimConditionSection={{ data: - // claim condition is common for all tokens - isErc721 && - // claim conditions is fetched - claimConditionQuery.isFetched && + // claim conditions data is present + claimConditionQuery.data && // token decimals is fetched if it should be fetched (shouldFetchTokenDecimals ? tokenDecimalsQuery.isFetched : true) ? { @@ -229,11 +234,17 @@ function ClaimableModule(props: ModuleInstanceProps) { } : undefined, setClaimCondition, - getClaimConditionErc1155, + tokenId, + isLoading: + claimConditionQuery.isLoading || + (!!shouldFetchTokenDecimals && tokenDecimalsQuery.isLoading), }} isOwnerAccount={!!ownerAccount} isErc721={isErc721} contractChainId={props.contract.chain.id} + setTokenId={setTokenId} + isValidTokenId={isValidTokenId} + noClaimConditionSet={noClaimConditionSet} mintSection={{ mint, }} @@ -246,6 +257,9 @@ export function ClaimableModuleUI( isOwnerAccount: boolean; isErc721: boolean; contractChainId: number; + setTokenId: Dispatch>; + isValidTokenId: boolean; + noClaimConditionSet: boolean; primarySaleRecipientSection: { setPrimarySaleRecipient: ( values: PrimarySaleRecipientFormValues, @@ -260,14 +274,15 @@ export function ClaimableModuleUI( mint: (values: MintFormValues) => Promise; }; claimConditionSection: { + tokenId: string; setClaimCondition: (values: ClaimConditionFormValues) => Promise; - getClaimConditionErc1155: (tokenId: string) => Promise; data: | { - claimCondition: ClaimConditionValue | undefined; + claimCondition: ClaimConditionValue; tokenDecimals: number | undefined; } | undefined; + isLoading: boolean; }; }, ) { @@ -296,25 +311,44 @@ export function ClaimableModuleUI( Claim Conditions - {!props.isErc721 || props.claimConditionSection.data ? ( - - ) : ( - + {!props.isErc721 && ( +
+
+ +

+ {props.isOwnerAccount + ? "View and Update claim conditions for given token ID" + : "View claim conditions for given token ID"} +

+ props.setTokenId(e.target.value)} /> +
+
)} + +
+ + {props.isValidTokenId && + props.claimConditionSection.data && + !props.claimConditionSection.isLoading && ( + + )} + {props.isValidTokenId && + props.claimConditionSection.isLoading && ( + + )} @@ -383,63 +417,48 @@ const defaultStartDate = addDays(new Date(), 7); const defaultEndDate = addDays(new Date(), 14); function ClaimConditionSection(props: { - claimCondition?: ClaimConditionValue; + claimCondition: ClaimConditionValue; update: (values: ClaimConditionFormValues) => Promise; isOwnerAccount: boolean; isErc721: boolean; chainId: number; tokenDecimals?: number; - getClaimConditionErc1155: (tokenId: string) => Promise; + tokenId: string; + noClaimConditionSet: boolean; }) { const { idToChain } = useAllChainsData(); const chain = idToChain.get(props.chainId); - const { claimCondition } = props; - - const tokenIdForm = useForm<{ tokenId: string }>({ - defaultValues: { - tokenId: "", - }, - }); - - const tokenId = tokenIdForm.watch("tokenId"); - - const claimConditionErc1155Query = useQuery({ - queryKey: ["claimConditionErc1155", props.chainId, tokenId], - queryFn: () => props.getClaimConditionErc1155(tokenId), - enabled: !props.isErc721 && BigInt(tokenId) >= 0n, - }); - - const conditions = props.isErc721 - ? claimCondition - : claimConditionErc1155Query.data; + const { tokenId, claimCondition } = props; + const [addClaimConditionButtonClicked, setAddClaimConditionButtonClicked] = + useState(false); const form = useForm({ resolver: zodResolver(claimConditionFormSchema), values: { tokenId, currencyAddress: - conditions?.currency === ZERO_ADDRESS ? "" : conditions?.currency, - pricePerToken: - conditions?.pricePerUnit && - conditions?.currency !== ZERO_ADDRESS && - props.tokenDecimals - ? Number(toTokens(conditions?.pricePerUnit, props.tokenDecimals)) - : 0, + claimCondition?.currency === ZERO_ADDRESS + ? NATIVE_TOKEN_ADDRESS // default to the native token address + : claimCondition?.currency, + // default case is zero state, so 0 // 10 ** 18 still results in 0 + pricePerToken: Number( + toTokens(claimCondition?.pricePerUnit, props.tokenDecimals || 18), + ), maxClaimableSupply: - conditions?.availableSupply.toString() === "0" || - conditions?.availableSupply.toString() === MAX_UINT_256 + claimCondition?.availableSupply.toString() === "0" || + claimCondition?.availableSupply.toString() === MAX_UINT_256 ? "" - : conditions?.availableSupply.toString() || "", + : claimCondition?.availableSupply.toString() || "", maxClaimablePerWallet: - conditions?.maxMintPerWallet.toString() === "0" || - conditions?.maxMintPerWallet.toString() === MAX_UINT_256 + claimCondition?.maxMintPerWallet.toString() === "0" || + claimCondition?.maxMintPerWallet.toString() === MAX_UINT_256 ? "" - : conditions?.maxMintPerWallet.toString() || "", - startTime: conditions?.startTimestamp - ? fromUnixTime(conditions?.startTimestamp) + : claimCondition?.maxMintPerWallet.toString() || "", + startTime: claimCondition?.startTimestamp + ? fromUnixTime(claimCondition?.startTimestamp) : defaultStartDate, - endTime: conditions?.endTimestamp - ? fromUnixTime(conditions?.endTimestamp) + endTime: claimCondition?.endTimestamp + ? fromUnixTime(claimCondition?.endTimestamp) : defaultEndDate, allowList: [], }, @@ -474,39 +493,30 @@ function ClaimConditionSection(props: { return (
- {!props.isErc721 && ( -
- - ( - - Token ID - - - - - - )} - /> - - + {props.noClaimConditionSet && !addClaimConditionButtonClicked && ( + <> + + + No Claim Condition Set + + You have not set a claim condition for this token. You can set a + claim condition by clicking the "Set Claim Condition" button. + + + + + )} -
- - {!props.isErc721 && - tokenId !== "" && - BigInt(tokenId) >= 0n && - claimConditionErc1155Query.isPending && ( - - )} - - {(props.isErc721 || - (tokenId !== "" && - BigInt(tokenId) >= 0n && - !claimConditionErc1155Query.isPending)) && ( + {(!props.noClaimConditionSet || addClaimConditionButtonClicked) && ( + +
- )} - {" "} - + {" "} + + )}
); } diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/CurrencySelector.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/CurrencySelector.tsx index d74941bab80..fb9408534e3 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/CurrencySelector.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/CurrencySelector.tsx @@ -166,7 +166,8 @@ export function CurrencySelector< ))} {isCustomCurrency && !isPaymentsSelector && - initialValue !== NATIVE_TOKEN_ADDRESS.toLowerCase() && ( + initialValue.toLowerCase() !== + NATIVE_TOKEN_ADDRESS.toLowerCase() && ( {initialValue} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx index 34f09ab8339..877096f930f 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx @@ -48,7 +48,7 @@ const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37"; const claimCondition = { availableSupply: BigInt(100), maxMintPerWallet: BigInt(10), - pricePerUnit: 10n, + pricePerUnit: 10000000000000000000n, // we get checksummed NATIVE_TOKEN_ADDRESS from claim condition query for native token currency: checksumAddress(NATIVE_TOKEN_ADDRESS), // last week @@ -64,6 +64,8 @@ function Component() { const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false); const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] = useState(false); + const [tokenId, setTokenId] = useState(""); + const [noClaimConditionSet, setNoClaimConditionSet] = useState(false); async function updatePrimarySaleRecipientStub( values: PrimarySaleRecipientFormValues, @@ -82,12 +84,6 @@ function Component() { await new Promise((resolve) => setTimeout(resolve, 1000)); } - async function getClaimConditionErc1155Stub() { - console.log("claim condition in stub: ", claimCondition); - await new Promise((resolve) => setTimeout(resolve, 1500)); - return claimCondition; - } - const removeMutation = useMutation({ mutationFn: async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -142,6 +138,13 @@ function Component() { id="isPrimarySaleRecipientLoading" label="Primary Sale Recipient Section Loading" /> + +
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-instance.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-instance.tsx index f548d79a50d..678a7c43f18 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-instance.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-instance.tsx @@ -39,7 +39,7 @@ export function ModuleInstance(props: ModuleInstanceProps) { return ; } - if (props.contractInfo.name.includes("Claimable-ignore")) { + if (props.contractInfo.name.includes("Claimable")) { return ; }