1
1
"use client" ;
2
2
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" ;
4
11
import { TransactionButton } from "components/buttons/TransactionButton" ;
5
12
import { useTrack } from "hooks/analytics/useTrack" ;
6
- import { useTxNotifications } from "hooks/useTxNotifications" ;
7
13
import { UploadIcon } from "lucide-react" ;
14
+ import { useState } from "react" ;
8
15
import { useForm } from "react-hook-form" ;
16
+ import { toast } from "sonner" ;
9
17
import type { ThirdwebContract } from "thirdweb" ;
10
18
import { multicall } from "thirdweb/extensions/common" ;
11
19
import { balanceOf , encodeSafeTransferFrom } from "thirdweb/extensions/erc1155" ;
12
20
import { useActiveAccount , useSendAndConfirmTransaction } from "thirdweb/react" ;
13
21
import { 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" ;
15
26
16
27
interface AirdropTabProps {
17
28
contract : ThirdwebContract ;
@@ -22,109 +33,113 @@ interface AirdropTabProps {
22
33
* This component must only take in ERC1155 contracts
23
34
*/
24
35
const AirdropTab : React . FC < AirdropTabProps > = ( { contract, tokenId } ) => {
25
- const account = useActiveAccount ( ) ;
26
-
27
36
const address = useActiveAccount ( ) ?. address ;
28
37
const { handleSubmit, setValue, watch, reset, formState } = useForm < {
29
38
addresses : AirdropAddressInput [ ] ;
30
39
} > ( {
31
40
defaultValues : { addresses : [ ] } ,
32
41
} ) ;
33
42
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 ( ) ;
45
44
const addresses = watch ( "addresses" ) ;
45
+ const [ open , setOpen ] = useState ( false ) ;
46
46
47
47
return (
48
48
< div className = "flex w-full flex-col gap-2" >
49
49
< form
50
50
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 (
70
70
`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
+ } ) ,
72
81
) ;
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" ) ;
73
113
}
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
- } ) ;
108
114
} ) }
109
115
>
110
116
< div className = "flex flex-col gap-2" >
111
117
< 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
- />
119
118
< 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 >
128
143
129
144
< Flex
130
145
gap = { 2 }
@@ -149,7 +164,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
149
164
< TransactionButton
150
165
txChainID = { contract . chain . id }
151
166
transactionCount = { 1 }
152
- isLoading = { isPending }
167
+ isLoading = { sendAndConfirmTx . isPending }
153
168
type = "submit"
154
169
colorScheme = "primary"
155
170
disabled = { ! ! address && addresses . length === 0 }
0 commit comments