11"use client" ;
22
3- import { Flex , useDisclosure } from "@chakra-ui/react" ;
3+ import {
4+ Sheet ,
5+ SheetContent ,
6+ SheetHeader ,
7+ SheetTitle ,
8+ SheetTrigger ,
9+ } from "@/components/ui/sheet" ;
10+ import { Flex } from "@chakra-ui/react" ;
411import { TransactionButton } from "components/buttons/TransactionButton" ;
512import { useTrack } from "hooks/analytics/useTrack" ;
6- import { useTxNotifications } from "hooks/useTxNotifications" ;
713import { UploadIcon } from "lucide-react" ;
14+ import { useState } from "react" ;
815import { useForm } from "react-hook-form" ;
16+ import { toast } from "sonner" ;
917import type { ThirdwebContract } from "thirdweb" ;
1018import { multicall } from "thirdweb/extensions/common" ;
1119import { balanceOf , encodeSafeTransferFrom } from "thirdweb/extensions/erc1155" ;
1220import { useActiveAccount , useSendAndConfirmTransaction } from "thirdweb/react" ;
1321import { Button , Text } from "tw-components" ;
14- import { type AirdropAddressInput , AirdropUpload } from "./airdrop-upload" ;
22+ import {
23+ type AirdropAddressInput ,
24+ AirdropUpload ,
25+ } from "../../../tokens/components/airdrop-upload" ;
1526
1627interface AirdropTabProps {
1728 contract : ThirdwebContract ;
@@ -22,109 +33,113 @@ interface AirdropTabProps {
2233 * This component must only take in ERC1155 contracts
2334 */
2435const AirdropTab : React . FC < AirdropTabProps > = ( { contract, tokenId } ) => {
25- const account = useActiveAccount ( ) ;
26-
2736 const address = useActiveAccount ( ) ?. address ;
2837 const { handleSubmit, setValue, watch, reset, formState } = useForm < {
2938 addresses : AirdropAddressInput [ ] ;
3039 } > ( {
3140 defaultValues : { addresses : [ ] } ,
3241 } ) ;
3342 const trackEvent = useTrack ( ) ;
34-
35- const { isOpen, onOpen, onClose } = useDisclosure ( ) ;
36-
37- const { mutate, isPending } = useSendAndConfirmTransaction ( ) ;
38-
39- const { onSuccess, onError } = useTxNotifications (
40- "Airdrop successful" ,
41- "Error transferring" ,
42- contract ,
43- ) ;
44-
43+ const sendAndConfirmTx = useSendAndConfirmTransaction ( ) ;
4544 const addresses = watch ( "addresses" ) ;
45+ const [ open , setOpen ] = useState ( false ) ;
4646
4747 return (
4848 < div className = "flex w-full flex-col gap-2" >
4949 < form
5050 onSubmit = { handleSubmit ( async ( _data ) => {
51- trackEvent ( {
52- category : "nft" ,
53- action : "airdrop " ,
54- label : "attempt " ,
55- contract_address : contract . address ,
56- token_id : tokenId ,
57- } ) ;
58- const totalOwned = await balanceOf ( {
59- contract ,
60- tokenId : BigInt ( tokenId ) ,
61- owner : account ?. address ?? "" ,
62- } ) ;
63- // todo: make a batch-transfer extension for erc1155?
64- const totalToAirdrop = _data . addresses . reduce ( ( prev , curr ) => {
65- return BigInt ( prev ) + BigInt ( curr ?. quantity || 1 ) ;
66- } , 0n ) ;
67- if ( totalOwned < totalToAirdrop ) {
68- return onError (
69- new Error (
51+ try {
52+ trackEvent ( {
53+ category : "nft " ,
54+ action : "airdrop " ,
55+ label : "attempt" ,
56+ contract_address : contract . address ,
57+ token_id : tokenId ,
58+ } ) ;
59+ const totalOwned = await balanceOf ( {
60+ contract ,
61+ tokenId : BigInt ( tokenId ) ,
62+ owner : address ?? "" ,
63+ } ) ;
64+ // todo: make a batch-transfer extension for erc1155?
65+ const totalToAirdrop = _data . addresses . reduce ( ( prev , curr ) => {
66+ return BigInt ( prev ) + BigInt ( curr ?. quantity || 1 ) ;
67+ } , 0n ) ;
68+ if ( totalOwned < totalToAirdrop ) {
69+ return toast . error (
7070 `The caller owns ${ totalOwned . toString ( ) } NFTs, but wants to airdrop ${ totalToAirdrop . toString ( ) } NFTs.` ,
71- ) ,
71+ ) ;
72+ }
73+ const data = _data . addresses . map ( ( { address : to , quantity } ) =>
74+ encodeSafeTransferFrom ( {
75+ from : address ?? "" ,
76+ to,
77+ value : BigInt ( quantity ) ,
78+ data : "0x" ,
79+ tokenId : BigInt ( tokenId ) ,
80+ } ) ,
7281 ) ;
82+ const transaction = multicall ( { contract, data } ) ;
83+ const promise = sendAndConfirmTx . mutateAsync ( transaction , {
84+ onSuccess : ( ) => {
85+ trackEvent ( {
86+ category : "nft" ,
87+ action : "airdrop" ,
88+ label : "success" ,
89+ contract_address : contract . address ,
90+ token_id : tokenId ,
91+ } ) ;
92+ reset ( ) ;
93+ } ,
94+ onError : ( error ) => {
95+ trackEvent ( {
96+ category : "nft" ,
97+ action : "airdrop" ,
98+ label : "success" ,
99+ contract_address : contract . address ,
100+ token_id : tokenId ,
101+ error,
102+ } ) ;
103+ } ,
104+ } ) ;
105+ toast . promise ( promise , {
106+ loading : "Airdropping NFTs" ,
107+ success : "Airdropped successfully" ,
108+ error : "Failed to airdrop" ,
109+ } ) ;
110+ } catch ( err ) {
111+ console . error ( err ) ;
112+ toast . error ( "Failed to airdrop NFTs" ) ;
73113 }
74- const data = _data . addresses . map ( ( { address : to , quantity } ) =>
75- encodeSafeTransferFrom ( {
76- from : account ?. address ?? "" ,
77- to,
78- value : BigInt ( quantity ) ,
79- data : "0x" ,
80- tokenId : BigInt ( tokenId ) ,
81- } ) ,
82- ) ;
83- const transaction = multicall ( { contract, data } ) ;
84- mutate ( transaction , {
85- onSuccess : ( ) => {
86- trackEvent ( {
87- category : "nft" ,
88- action : "airdrop" ,
89- label : "success" ,
90- contract_address : contract . address ,
91- token_id : tokenId ,
92- } ) ;
93- onSuccess ( ) ;
94- reset ( ) ;
95- } ,
96- onError : ( error ) => {
97- trackEvent ( {
98- category : "nft" ,
99- action : "airdrop" ,
100- label : "success" ,
101- contract_address : contract . address ,
102- token_id : tokenId ,
103- error,
104- } ) ;
105- onError ( error ) ;
106- } ,
107- } ) ;
108114 } ) }
109115 >
110116 < div className = "flex flex-col gap-2" >
111117 < div className = "mb-3 flex w-full flex-col gap-6 md:flex-row" >
112- < AirdropUpload
113- isOpen = { isOpen }
114- onClose = { onClose }
115- setAirdrop = { ( value ) =>
116- setValue ( "addresses" , value , { shouldDirty : true } )
117- }
118- />
119118 < Flex direction = { { base : "column" , md : "row" } } gap = { 4 } >
120- < Button
121- colorScheme = "primary"
122- borderRadius = "md"
123- onClick = { onOpen }
124- rightIcon = { < UploadIcon className = "size-5" /> }
125- >
126- Upload addresses
127- </ Button >
119+ < Sheet open = { open } onOpenChange = { setOpen } >
120+ < SheetTrigger asChild >
121+ < Button
122+ colorScheme = "primary"
123+ borderRadius = "md"
124+ rightIcon = { < UploadIcon className = "size-5" /> }
125+ >
126+ Upload addresses
127+ </ Button >
128+ </ SheetTrigger >
129+ < SheetContent className = "w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]" >
130+ < SheetHeader >
131+ < SheetTitle className = "mb-5 text-left" >
132+ Aidrop NFTs
133+ </ SheetTitle >
134+ </ SheetHeader >
135+ < AirdropUpload
136+ onClose = { ( ) => setOpen ( false ) }
137+ setAirdrop = { ( value ) =>
138+ setValue ( "addresses" , value , { shouldDirty : true } )
139+ }
140+ />
141+ </ SheetContent >
142+ </ Sheet >
128143
129144 < Flex
130145 gap = { 2 }
@@ -149,7 +164,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
149164 < TransactionButton
150165 txChainID = { contract . chain . id }
151166 transactionCount = { 1 }
152- isLoading = { isPending }
167+ isLoading = { sendAndConfirmTx . isPending }
153168 type = "submit"
154169 colorScheme = "primary"
155170 disabled = { ! ! address && addresses . length === 0 }
0 commit comments