1
1
"use client" ;
2
2
3
- import { Badge } from "@/components/ui/badge" ;
3
+ import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage" ;
4
+ import { Spinner } from "@/components/ui/Spinner/Spinner" ;
4
5
import { Button } from "@/components/ui/button" ;
5
- import { Card } from "@/components/ui/card" ;
6
+ import {
7
+ Dialog ,
8
+ DialogContent ,
9
+ DialogHeader ,
10
+ DialogTitle ,
11
+ DialogTrigger ,
12
+ } from "@/components/ui/dialog" ;
6
13
import { useDashboardRouter } from "@/lib/DashboardRouter" ;
7
14
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi" ;
8
- import {
9
- Divider ,
10
- Flex ,
11
- Modal ,
12
- ModalBody ,
13
- ModalCloseButton ,
14
- ModalContent ,
15
- ModalHeader ,
16
- ModalOverlay ,
17
- Spinner ,
18
- useDisclosure ,
19
- } from "@chakra-ui/react" ;
20
15
import { useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
21
16
import { SourcesPanel } from "components/contract-components/shared/sources-panel" ;
22
17
import { useContractSources } from "contract-ui/hooks/useContractSources" ;
23
- import { CircleCheckIcon , CircleXIcon } from "lucide-react" ;
24
- import { useMemo , useState } from "react" ;
18
+ import {
19
+ CircleCheckIcon ,
20
+ CircleXIcon ,
21
+ RefreshCcwIcon ,
22
+ ShieldCheckIcon ,
23
+ } from "lucide-react" ;
24
+ import { useMemo } from "react" ;
25
25
import { toast } from "sonner" ;
26
26
import type { ThirdwebContract } from "thirdweb" ;
27
- import { Heading } from "tw-components" ;
28
27
29
- interface ContractSourcesPageProps {
30
- contract : ThirdwebContract ;
31
- }
32
-
33
- interface VerificationResult {
28
+ type VerificationResult = {
34
29
explorerUrl : string ;
35
30
success : boolean ;
36
31
alreadyVerified : boolean ;
37
32
error ?: string ;
38
- }
33
+ } ;
39
34
40
35
export async function verifyContract ( contract : ThirdwebContract ) {
41
36
try {
@@ -63,111 +58,80 @@ export async function verifyContract(contract: ThirdwebContract) {
63
58
}
64
59
}
65
60
66
- interface ConnectorModalProps {
67
- isOpen : boolean ;
68
- onClose : ( ) => void ;
61
+ function VerifyContractModalContent ( {
62
+ contract ,
63
+ } : {
69
64
contract : ThirdwebContract ;
70
- }
71
-
72
- const VerifyContractModal : React . FC <
73
- ConnectorModalProps & { resetSignal : number }
74
- > = ( { isOpen, onClose, contract, resetSignal } ) => {
65
+ } ) {
75
66
const verifyQuery = useQuery ( {
76
- queryKey : [
77
- "verify-contract" ,
78
- contract . chain . id ,
79
- contract . address ,
80
- resetSignal ,
81
- ] ,
67
+ queryKey : [ "verify-contract" , contract . chain . id , contract . address ] ,
82
68
queryFn : ( ) => verifyContract ( contract ) ,
83
- enabled : isOpen ,
84
69
} ) ;
85
70
86
71
return (
87
- < Modal isOpen = { isOpen } onClose = { onClose } isCentered >
88
- < ModalOverlay />
89
- < ModalContent
90
- className = "!bg-background rounded-lg border border-border"
91
- pb = { 2 }
92
- mx = { { base : 4 , md : 0 } }
93
- >
94
- < ModalHeader >
95
- < Flex gap = { 2 } align = "center" >
96
- < Heading size = "subtitle.md" > Contract Verification</ Heading >
97
- < Badge > beta</ Badge >
98
- </ Flex >
99
- </ ModalHeader >
100
- < ModalCloseButton mt = { 2 } />
101
- < Divider mb = { 4 } />
102
- < ModalBody py = { 4 } >
103
- < Flex flexDir = "column" >
104
- { verifyQuery . isPending && (
105
- < Flex gap = { 2 } align = "center" >
106
- < Spinner color = "purple.500" size = "sm" />
107
- < Heading size = "label.md" > Verifying...</ Heading >
108
- </ Flex >
109
- ) }
110
- { verifyQuery ?. error ? (
111
- < Flex gap = { 2 } align = "center" >
112
- < CircleXIcon className = "size-4 text-red-600" />
113
- < Heading size = "label.md" >
114
- { verifyQuery ?. error . toString ( ) }
115
- </ Heading >
116
- </ Flex >
117
- ) : null }
72
+ < div className = "flex flex-col p-6" >
73
+ { verifyQuery . isPending && (
74
+ < div className = "flex min-h-24 items-center justify-center" >
75
+ < div className = "flex items-center gap-2" >
76
+ < Spinner className = "size-4" />
77
+ < p className = "font-medium text-sm" > Verifying</ p >
78
+ </ div >
79
+ </ div >
80
+ ) }
118
81
119
- { verifyQuery . data ?. results
120
- ? verifyQuery . data ?. results . map (
121
- ( result : VerificationResult , index : number ) => (
122
- // biome-ignore lint/suspicious/noArrayIndexKey: FIXME
123
- < Flex key = { index } gap = { 2 } align = "center" mb = { 4 } >
124
- { result . success && (
125
- < >
126
- < CircleCheckIcon className = "size-4 text-green-600" />
127
- { result . alreadyVerified && (
128
- < Heading size = "label.md" >
129
- { result . explorerUrl } : Already verified
130
- </ Heading >
131
- ) }
132
- { ! result . alreadyVerified && (
133
- < Heading size = "label.md" >
134
- { result . explorerUrl } : Verification successful
135
- </ Heading >
136
- ) }
137
- </ >
138
- ) }
139
- { ! result . success && (
140
- < >
141
- < CircleXIcon className = "size-4 text-red-600" />
142
- < Heading size = "label.md" >
143
- { `${ result . explorerUrl } : Verification failed` }
144
- </ Heading >
145
- </ >
146
- ) }
147
- </ Flex >
148
- ) ,
149
- )
150
- : null }
151
- </ Flex >
152
- </ ModalBody >
153
- </ ModalContent >
154
- </ Modal >
155
- ) ;
156
- } ;
157
-
158
- export const ContractSourcesPage : React . FC < ContractSourcesPageProps > = ( {
159
- contract,
160
- } ) => {
161
- const [ resetSignal , setResetSignal ] = useState ( 0 ) ;
82
+ { verifyQuery ?. error ? (
83
+ < div className = "flex min-h-24 items-center justify-center" >
84
+ < div className = "flex items-center gap-2" >
85
+ < CircleXIcon className = "size-4 text-red-600" />
86
+ < p className = "font-medium text-sm" >
87
+ { verifyQuery ?. error . toString ( ) }
88
+ </ p >
89
+ </ div >
90
+ </ div >
91
+ ) : null }
162
92
163
- const { isOpen, onOpen, onClose } = useDisclosure ( ) ;
93
+ { verifyQuery . data ?. results ? (
94
+ < div className = "flex flex-col gap-2" >
95
+ { verifyQuery . data . results . map (
96
+ ( result : VerificationResult , index : number ) => (
97
+ // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
98
+ < div key = { index } className = "flex items-center gap-2" >
99
+ { result . success && (
100
+ < >
101
+ < CircleCheckIcon className = "size-4 text-green-600" />
102
+ { result . alreadyVerified && (
103
+ < p className = "font-medium text-sm" >
104
+ { result . explorerUrl } : Already verified
105
+ </ p >
106
+ ) }
107
+ { ! result . alreadyVerified && (
108
+ < p className = "font-medium text-sm" >
109
+ { result . explorerUrl } : Verification successful
110
+ </ p >
111
+ ) }
112
+ </ >
113
+ ) }
164
114
165
- const handleClose = ( ) => {
166
- onClose ( ) ;
167
- // Increment to reset the query in the child component
168
- setResetSignal ( ( prev : number ) => prev + 1 ) ;
169
- } ;
115
+ { ! result . success && (
116
+ < >
117
+ < CircleXIcon className = "size-4 text-red-600" />
118
+ < p className = "font-medium text-sm" >
119
+ { `${ result . explorerUrl } : Verification failed` }
120
+ </ p >
121
+ </ >
122
+ ) }
123
+ </ div >
124
+ ) ,
125
+ ) }
126
+ </ div >
127
+ ) : null }
128
+ </ div >
129
+ ) ;
130
+ }
170
131
132
+ export function ContractSourcesPage ( {
133
+ contract,
134
+ } : { contract : ThirdwebContract } ) {
171
135
const contractSourcesQuery = useContractSources ( contract ) ;
172
136
const abiQuery = useResolveContractAbi ( contract ) ;
173
137
@@ -187,44 +151,49 @@ export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
187
151
. reverse ( ) ;
188
152
} , [ contractSourcesQuery . data ] ) ;
189
153
190
- if ( ! contractSourcesQuery || contractSourcesQuery ?. isPending ) {
191
- return (
192
- < Flex direction = "row" align = "center" gap = { 2 } >
193
- < Spinner color = "purple.500" size = "xs" />
194
- < Heading size = "title.sm" > Loading...</ Heading >
195
- </ Flex >
196
- ) ;
154
+ if ( ! contractSourcesQuery || contractSourcesQuery . isPending ) {
155
+ return < GenericLoadingPage /> ;
197
156
}
198
157
199
158
return (
200
- < >
201
- < VerifyContractModal
202
- isOpen = { isOpen }
203
- onClose = { ( ) => handleClose ( ) }
204
- contract = { contract }
205
- resetSignal = { resetSignal }
206
- />
207
-
208
- < Flex direction = "column" gap = { 8 } >
209
- < Flex direction = "row" alignItems = "center" gap = { 2 } >
210
- < Heading size = "title.sm" flex = { 1 } >
211
- Sources
212
- </ Heading >
159
+ < div >
160
+ < div className = "flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between" >
161
+ < div >
162
+ < h2 className = "font-semibold text-2xl tracking-tight" > Sources</ h2 >
163
+ < p className = "text-muted-foreground text-sm" >
164
+ View ABI and source code for the contract
165
+ </ p >
166
+ </ div >
167
+ < div className = "flex items-center gap-3" >
213
168
< RefreshContractMetadataButton
214
169
chainId = { contract . chain . id }
215
170
contractAddress = { contract . address }
216
171
/>
217
- < Button variant = "primary" onClick = { onOpen } >
218
- Verify contract
219
- </ Button >
220
- </ Flex >
221
- < Card >
222
- < SourcesPanel sources = { sources } abi = { abiQuery . data } />
223
- </ Card >
224
- </ Flex >
225
- </ >
172
+
173
+ < Dialog >
174
+ < DialogTrigger asChild >
175
+ < Button className = "gap-2" size = "sm" >
176
+ < ShieldCheckIcon className = "size-4" />
177
+ Verify contract
178
+ </ Button >
179
+ </ DialogTrigger >
180
+ < DialogContent className = "gap-0 overflow-hidden p-0" >
181
+ < DialogHeader className = "border-b p-6" >
182
+ < DialogTitle > Verify Contract</ DialogTitle >
183
+ </ DialogHeader >
184
+ < VerifyContractModalContent contract = { contract } />
185
+ </ DialogContent >
186
+ </ Dialog >
187
+ </ div >
188
+ </ div >
189
+
190
+ < div className = "h-4" />
191
+ < div className = "rounded-lg border bg-card" >
192
+ < SourcesPanel sources = { sources } abi = { abiQuery . data } />
193
+ </ div >
194
+ </ div >
226
195
) ;
227
- } ;
196
+ }
228
197
229
198
function RefreshContractMetadataButton ( props : {
230
199
chainId : number ;
@@ -270,14 +239,19 @@ function RefreshContractMetadataButton(props: {
270
239
onClick = { ( ) => {
271
240
toast . promise ( contractCacheMutation . mutateAsync ( ) , {
272
241
duration : 5000 ,
273
- loading : "Refreshing contract data..." ,
274
- success : ( ) => "Contract data refreshed!" ,
242
+ success : ( ) => "Contract refreshed successfully" ,
275
243
error : ( e ) => e ?. message || "Failed to refresh contract data." ,
276
244
} ) ;
277
245
} }
278
- className = "w-[182px]"
246
+ size = "sm"
247
+ className = "gap-2 bg-card"
279
248
>
280
- { contractCacheMutation . isPending ? < Spinner /> : "Refresh Contract Data" }
249
+ { contractCacheMutation . isPending ? (
250
+ < Spinner className = "size-4" />
251
+ ) : (
252
+ < RefreshCcwIcon className = "size-4" />
253
+ ) }
254
+ Refresh Contract < span className = "max-sm:hidden" > Data </ span >
281
255
</ Button >
282
256
) ;
283
257
}
0 commit comments