Skip to content

Commit 92e55c8

Browse files
committed
[Dashboard] Feature: Updated Claimable (#5413)
- 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 <!-- start pr-codex --> --- ## 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}` <!-- end pr-codex -->
1 parent 5de5c44 commit 92e55c8

File tree

4 files changed

+153
-134
lines changed

4 files changed

+153
-134
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx

Lines changed: 128 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use client";
2+
23
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
34
import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange";
45
import {
@@ -7,7 +8,7 @@ import {
78
AccordionItem,
89
AccordionTrigger,
910
} from "@/components/ui/accordion";
10-
import { Alert, AlertTitle } from "@/components/ui/alert";
11+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
1112
import { Button } from "@/components/ui/button";
1213
import {
1314
Form,
@@ -18,19 +19,26 @@ import {
1819
FormMessage,
1920
} from "@/components/ui/form";
2021
import { Input } from "@/components/ui/input";
22+
import { Label } from "@/components/ui/label";
2123
import { Separator } from "@/components/ui/separator";
2224
import { Skeleton } from "@/components/ui/skeleton";
2325
import { ToolTipLabel } from "@/components/ui/tooltip";
2426
import { zodResolver } from "@hookform/resolvers/zod";
25-
import { useMutation, useQuery } from "@tanstack/react-query";
27+
import { useMutation } from "@tanstack/react-query";
2628
import { TransactionButton } from "components/buttons/TransactionButton";
2729
import { addDays, fromUnixTime } from "date-fns";
2830
import { useAllChainsData } from "hooks/chains/allChains";
2931
import { useTxNotifications } from "hooks/useTxNotifications";
3032
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
31-
import { useCallback } from "react";
33+
import {
34+
type Dispatch,
35+
type SetStateAction,
36+
useCallback,
37+
useState,
38+
} from "react";
3239
import { useFieldArray, useForm } from "react-hook-form";
3340
import {
41+
NATIVE_TOKEN_ADDRESS,
3442
type PreparedTransaction,
3543
ZERO_ADDRESS,
3644
getContract,
@@ -57,22 +65,15 @@ export type ClaimConditionValue = {
5765
auxData: string;
5866
};
5967

60-
type ClaimCondition = {
61-
availableSupply: bigint;
62-
allowlistMerkleRoot: `0x${string}`;
63-
pricePerUnit: bigint;
64-
currency: string;
65-
maxMintPerWallet: bigint;
66-
startTimestamp: number;
67-
endTimestamp: number;
68-
auxData: string;
69-
};
68+
const positiveIntegerRegex = /^[0-9]\d*$/;
7069

7170
function ClaimableModule(props: ModuleInstanceProps) {
7271
const { contract, ownerAccount } = props;
7372
const account = useActiveAccount();
73+
const [tokenId, setTokenId] = useState<string>("");
7474

7575
const isErc721 = props.contractInfo.name === "ClaimableERC721";
76+
const isValidTokenId = positiveIntegerRegex.test(tokenId);
7677

7778
const primarySaleRecipientQuery = useReadContract(
7879
isErc721 ? ClaimableERC721.getSaleConfig : ClaimableERC1155.getSaleConfig,
@@ -82,20 +83,27 @@ function ClaimableModule(props: ModuleInstanceProps) {
8283
);
8384

8485
const claimConditionQuery = useReadContract(
85-
ClaimableERC721.getClaimCondition,
86+
isErc721
87+
? ClaimableERC721.getClaimCondition
88+
: ClaimableERC1155.getClaimCondition,
8689
{
90+
tokenId: positiveIntegerRegex.test(tokenId) ? BigInt(tokenId) : 0n,
8791
contract: contract,
8892
queryOptions: {
89-
enabled: isErc721,
93+
enabled: isErc721 || (!!tokenId && isValidTokenId),
9094
},
9195
},
9296
);
9397

94-
const getClaimConditionErc1155 = (tokenId: string) =>
95-
ClaimableERC1155.getClaimCondition({
96-
contract: contract,
97-
tokenId: BigInt(tokenId),
98-
});
98+
const noClaimConditionSet =
99+
claimConditionQuery.data?.availableSupply === 0n &&
100+
claimConditionQuery.data?.allowlistMerkleRoot ===
101+
"0x0000000000000000000000000000000000000000000000000000000000000000" &&
102+
claimConditionQuery.data?.pricePerUnit === 0n &&
103+
claimConditionQuery.data?.currency === ZERO_ADDRESS &&
104+
claimConditionQuery.data?.maxMintPerWallet === 0n &&
105+
claimConditionQuery.data?.startTimestamp === 0 &&
106+
claimConditionQuery.data?.endTimestamp === 0;
99107

100108
const currencyContract = getContract({
101109
address: claimConditionQuery.data?.currency || "",
@@ -104,7 +112,6 @@ function ClaimableModule(props: ModuleInstanceProps) {
104112
});
105113

106114
const shouldFetchTokenDecimals =
107-
isErc721 &&
108115
claimConditionQuery.data &&
109116
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
110117

@@ -217,10 +224,8 @@ function ClaimableModule(props: ModuleInstanceProps) {
217224
}}
218225
claimConditionSection={{
219226
data:
220-
// claim condition is common for all tokens
221-
isErc721 &&
222-
// claim conditions is fetched
223-
claimConditionQuery.isFetched &&
227+
// claim conditions data is present
228+
claimConditionQuery.data &&
224229
// token decimals is fetched if it should be fetched
225230
(shouldFetchTokenDecimals ? tokenDecimalsQuery.isFetched : true)
226231
? {
@@ -229,11 +234,17 @@ function ClaimableModule(props: ModuleInstanceProps) {
229234
}
230235
: undefined,
231236
setClaimCondition,
232-
getClaimConditionErc1155,
237+
tokenId,
238+
isLoading:
239+
claimConditionQuery.isLoading ||
240+
(!!shouldFetchTokenDecimals && tokenDecimalsQuery.isLoading),
233241
}}
234242
isOwnerAccount={!!ownerAccount}
235243
isErc721={isErc721}
236244
contractChainId={props.contract.chain.id}
245+
setTokenId={setTokenId}
246+
isValidTokenId={isValidTokenId}
247+
noClaimConditionSet={noClaimConditionSet}
237248
mintSection={{
238249
mint,
239250
}}
@@ -246,6 +257,9 @@ export function ClaimableModuleUI(
246257
isOwnerAccount: boolean;
247258
isErc721: boolean;
248259
contractChainId: number;
260+
setTokenId: Dispatch<SetStateAction<string>>;
261+
isValidTokenId: boolean;
262+
noClaimConditionSet: boolean;
249263
primarySaleRecipientSection: {
250264
setPrimarySaleRecipient: (
251265
values: PrimarySaleRecipientFormValues,
@@ -260,14 +274,15 @@ export function ClaimableModuleUI(
260274
mint: (values: MintFormValues) => Promise<void>;
261275
};
262276
claimConditionSection: {
277+
tokenId: string;
263278
setClaimCondition: (values: ClaimConditionFormValues) => Promise<void>;
264-
getClaimConditionErc1155: (tokenId: string) => Promise<ClaimCondition>;
265279
data:
266280
| {
267-
claimCondition: ClaimConditionValue | undefined;
281+
claimCondition: ClaimConditionValue;
268282
tokenDecimals: number | undefined;
269283
}
270284
| undefined;
285+
isLoading: boolean;
271286
};
272287
},
273288
) {
@@ -296,25 +311,44 @@ export function ClaimableModuleUI(
296311
Claim Conditions
297312
</AccordionTrigger>
298313
<AccordionContent className="px-1">
299-
{!props.isErc721 || props.claimConditionSection.data ? (
300-
<ClaimConditionSection
301-
isOwnerAccount={props.isOwnerAccount}
302-
claimCondition={
303-
props.claimConditionSection.data?.claimCondition
304-
}
305-
update={props.claimConditionSection.setClaimCondition}
306-
isErc721={props.isErc721}
307-
chainId={props.contractChainId}
308-
tokenDecimals={
309-
props.claimConditionSection.data?.tokenDecimals
310-
}
311-
getClaimConditionErc1155={
312-
props.claimConditionSection.getClaimConditionErc1155
313-
}
314-
/>
315-
) : (
316-
<Skeleton className="h-[350px]" />
314+
{!props.isErc721 && (
315+
<div className="flex flex-col gap-6">
316+
<div className="flex-1 space-y-1">
317+
<Label>Token ID</Label>
318+
<p className="text-muted-foreground text-sm">
319+
{props.isOwnerAccount
320+
? "View and Update claim conditions for given token ID"
321+
: "View claim conditions for given token ID"}
322+
</p>
323+
<Input onChange={(e) => props.setTokenId(e.target.value)} />
324+
</div>
325+
</div>
317326
)}
327+
328+
<div className="h-6" />
329+
330+
{props.isValidTokenId &&
331+
props.claimConditionSection.data &&
332+
!props.claimConditionSection.isLoading && (
333+
<ClaimConditionSection
334+
isOwnerAccount={props.isOwnerAccount}
335+
claimCondition={
336+
props.claimConditionSection.data.claimCondition
337+
}
338+
update={props.claimConditionSection.setClaimCondition}
339+
isErc721={props.isErc721}
340+
chainId={props.contractChainId}
341+
noClaimConditionSet={props.noClaimConditionSet}
342+
tokenDecimals={
343+
props.claimConditionSection.data?.tokenDecimals
344+
}
345+
tokenId={props.claimConditionSection.tokenId}
346+
/>
347+
)}
348+
{props.isValidTokenId &&
349+
props.claimConditionSection.isLoading && (
350+
<Skeleton className="h-[350px]" />
351+
)}
318352
</AccordionContent>
319353
</AccordionItem>
320354

@@ -383,63 +417,48 @@ const defaultStartDate = addDays(new Date(), 7);
383417
const defaultEndDate = addDays(new Date(), 14);
384418

385419
function ClaimConditionSection(props: {
386-
claimCondition?: ClaimConditionValue;
420+
claimCondition: ClaimConditionValue;
387421
update: (values: ClaimConditionFormValues) => Promise<void>;
388422
isOwnerAccount: boolean;
389423
isErc721: boolean;
390424
chainId: number;
391425
tokenDecimals?: number;
392-
getClaimConditionErc1155: (tokenId: string) => Promise<ClaimCondition>;
426+
tokenId: string;
427+
noClaimConditionSet: boolean;
393428
}) {
394429
const { idToChain } = useAllChainsData();
395430
const chain = idToChain.get(props.chainId);
396-
const { claimCondition } = props;
397-
398-
const tokenIdForm = useForm<{ tokenId: string }>({
399-
defaultValues: {
400-
tokenId: "",
401-
},
402-
});
403-
404-
const tokenId = tokenIdForm.watch("tokenId");
405-
406-
const claimConditionErc1155Query = useQuery({
407-
queryKey: ["claimConditionErc1155", props.chainId, tokenId],
408-
queryFn: () => props.getClaimConditionErc1155(tokenId),
409-
enabled: !props.isErc721 && BigInt(tokenId) >= 0n,
410-
});
411-
412-
const conditions = props.isErc721
413-
? claimCondition
414-
: claimConditionErc1155Query.data;
431+
const { tokenId, claimCondition } = props;
432+
const [addClaimConditionButtonClicked, setAddClaimConditionButtonClicked] =
433+
useState(false);
415434

416435
const form = useForm<ClaimConditionFormValues>({
417436
resolver: zodResolver(claimConditionFormSchema),
418437
values: {
419438
tokenId,
420439
currencyAddress:
421-
conditions?.currency === ZERO_ADDRESS ? "" : conditions?.currency,
422-
pricePerToken:
423-
conditions?.pricePerUnit &&
424-
conditions?.currency !== ZERO_ADDRESS &&
425-
props.tokenDecimals
426-
? Number(toTokens(conditions?.pricePerUnit, props.tokenDecimals))
427-
: 0,
440+
claimCondition?.currency === ZERO_ADDRESS
441+
? NATIVE_TOKEN_ADDRESS // default to the native token address
442+
: claimCondition?.currency,
443+
// default case is zero state, so 0 // 10 ** 18 still results in 0
444+
pricePerToken: Number(
445+
toTokens(claimCondition?.pricePerUnit, props.tokenDecimals || 18),
446+
),
428447
maxClaimableSupply:
429-
conditions?.availableSupply.toString() === "0" ||
430-
conditions?.availableSupply.toString() === MAX_UINT_256
448+
claimCondition?.availableSupply.toString() === "0" ||
449+
claimCondition?.availableSupply.toString() === MAX_UINT_256
431450
? ""
432-
: conditions?.availableSupply.toString() || "",
451+
: claimCondition?.availableSupply.toString() || "",
433452
maxClaimablePerWallet:
434-
conditions?.maxMintPerWallet.toString() === "0" ||
435-
conditions?.maxMintPerWallet.toString() === MAX_UINT_256
453+
claimCondition?.maxMintPerWallet.toString() === "0" ||
454+
claimCondition?.maxMintPerWallet.toString() === MAX_UINT_256
436455
? ""
437-
: conditions?.maxMintPerWallet.toString() || "",
438-
startTime: conditions?.startTimestamp
439-
? fromUnixTime(conditions?.startTimestamp)
456+
: claimCondition?.maxMintPerWallet.toString() || "",
457+
startTime: claimCondition?.startTimestamp
458+
? fromUnixTime(claimCondition?.startTimestamp)
440459
: defaultStartDate,
441-
endTime: conditions?.endTimestamp
442-
? fromUnixTime(conditions?.endTimestamp)
460+
endTime: claimCondition?.endTimestamp
461+
? fromUnixTime(claimCondition?.endTimestamp)
443462
: defaultEndDate,
444463
allowList: [],
445464
},
@@ -474,39 +493,30 @@ function ClaimConditionSection(props: {
474493

475494
return (
476495
<div className="flex flex-col gap-6">
477-
{!props.isErc721 && (
478-
<Form {...tokenIdForm}>
479-
<form>
480-
<FormField
481-
control={tokenIdForm.control}
482-
name="tokenId"
483-
render={({ field }) => (
484-
<FormItem className="flex-1">
485-
<FormLabel>Token ID</FormLabel>
486-
<FormControl>
487-
<Input {...field} disabled={!props.isOwnerAccount} />
488-
</FormControl>
489-
<FormMessage />
490-
</FormItem>
491-
)}
492-
/>
493-
</form>
494-
</Form>
496+
{props.noClaimConditionSet && !addClaimConditionButtonClicked && (
497+
<>
498+
<Alert variant="warning">
499+
<CircleAlertIcon className="size-5 max-sm:hidden" />
500+
<AlertTitle>No Claim Condition Set</AlertTitle>
501+
<AlertDescription>
502+
You have not set a claim condition for this token. You can set a
503+
claim condition by clicking the "Set Claim Condition" button.
504+
</AlertDescription>
505+
</Alert>
506+
507+
<Button
508+
onClick={() => setAddClaimConditionButtonClicked(true)}
509+
variant="outline"
510+
className="w-full"
511+
>
512+
Add Claim Condition
513+
</Button>
514+
</>
495515
)}
496516

497-
<Form {...form}>
498-
<form onSubmit={form.handleSubmit(onSubmit)}>
499-
{!props.isErc721 &&
500-
tokenId !== "" &&
501-
BigInt(tokenId) >= 0n &&
502-
claimConditionErc1155Query.isPending && (
503-
<Skeleton className="h-[350px]" />
504-
)}
505-
506-
{(props.isErc721 ||
507-
(tokenId !== "" &&
508-
BigInt(tokenId) >= 0n &&
509-
!claimConditionErc1155Query.isPending)) && (
517+
{(!props.noClaimConditionSet || addClaimConditionButtonClicked) && (
518+
<Form {...form}>
519+
<form onSubmit={form.handleSubmit(onSubmit)}>
510520
<div className="flex flex-col gap-6">
511521
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
512522
<FormField
@@ -668,9 +678,9 @@ function ClaimConditionSection(props: {
668678
</TransactionButton>
669679
</div>
670680
</div>
671-
)}
672-
</form>{" "}
673-
</Form>
681+
</form>{" "}
682+
</Form>
683+
)}
674684
</div>
675685
);
676686
}

0 commit comments

Comments
 (0)