11"use client" ;
2+ import { CopyTextButton } from "@/components/ui/CopyTextButton" ;
23import { Spinner } from "@/components/ui/Spinner/Spinner" ;
34import { Button } from "@/components/ui/button" ;
4- import { Form , FormControl , FormField , FormItem } from "@/components/ui/form" ;
5+ import {
6+ Dialog ,
7+ DialogContent ,
8+ DialogDescription ,
9+ DialogHeader ,
10+ DialogTitle ,
11+ DialogTrigger ,
12+ } from "@/components/ui/dialog" ;
13+ import {
14+ Form ,
15+ FormControl ,
16+ FormField ,
17+ FormItem ,
18+ FormLabel ,
19+ FormMessage ,
20+ } from "@/components/ui/form" ;
21+ import { Input } from "@/components/ui/input" ;
522import {
623 THIRDWEB_ENGINE_FAUCET_WALLET ,
724 TURNSTILE_SITE_KEY ,
825} from "@/constants/env" ;
926import { useThirdwebClient } from "@/constants/thirdweb.client" ;
27+ import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet" ;
1028import type { Account } from "@3rdweb-sdk/react/hooks/useApi" ;
29+ import { zodResolver } from "@hookform/resolvers/zod" ;
1130import { Turnstile } from "@marsidev/react-turnstile" ;
1231import { useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
1332import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType" ;
@@ -17,9 +36,21 @@ import Link from "next/link";
1736import { usePathname } from "next/navigation" ;
1837import { useForm } from "react-hook-form" ;
1938import { toast } from "sonner" ;
20- import { toUnits } from "thirdweb" ;
39+ import {
40+ type ThirdwebClient ,
41+ prepareTransaction ,
42+ toUnits ,
43+ toWei ,
44+ } from "thirdweb" ;
2145import type { ChainMetadata } from "thirdweb/chains" ;
22- import { useActiveAccount , useWalletBalance } from "thirdweb/react" ;
46+ import type { Chain } from "thirdweb/chains" ;
47+ import {
48+ useActiveAccount ,
49+ useActiveWalletChain ,
50+ useSendTransaction ,
51+ useSwitchActiveWalletChain ,
52+ useWalletBalance ,
53+ } from "thirdweb/react" ;
2354import { z } from "zod" ;
2455import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired" ;
2556
@@ -137,6 +168,35 @@ export function FaucetButton({
137168
138169 const form = useForm < z . infer < typeof claimFaucetSchema > > ( ) ;
139170
171+ // loading state
172+ if ( faucetWalletBalanceQuery . isPending || canClaimFaucetQuery . isPending ) {
173+ return (
174+ < Button variant = "outline" className = "w-full gap-2" >
175+ Checking Faucet < Spinner className = "size-3" />
176+ </ Button >
177+ ) ;
178+ }
179+
180+ // faucet is empty
181+ if ( isFaucetEmpty ) {
182+ return (
183+ < div className = "w-full" >
184+ < div className = "mb-3 text-center text-muted-foreground text-sm" >
185+ Faucet is empty right now
186+ </ div >
187+ < SendFundsToFaucetModalButton
188+ chain = { definedChain }
189+ isLoggedIn = { ! ! twAccount }
190+ client = { client }
191+ chainMeta = { chain }
192+ onFaucetRefill = { ( ) => {
193+ faucetWalletBalanceQuery . refetch ( ) ;
194+ } }
195+ />
196+ </ div >
197+ ) ;
198+ }
199+
140200 // Force users to log in to claim the faucet
141201 if ( ! address || ! twAccount ) {
142202 return (
@@ -164,24 +224,6 @@ export function FaucetButton({
164224 ) ;
165225 }
166226
167- // loading state
168- if ( faucetWalletBalanceQuery . isPending || canClaimFaucetQuery . isPending ) {
169- return (
170- < Button variant = "outline" className = "w-full gap-2" >
171- Checking Faucet < Spinner className = "size-3" />
172- </ Button >
173- ) ;
174- }
175-
176- // faucet is empty
177- if ( isFaucetEmpty ) {
178- return (
179- < Button variant = "outline" disabled className = "!opacity-100 w-full" >
180- Faucet is empty right now
181- </ Button >
182- ) ;
183- }
184-
185227 // Can not claim
186228 if ( canClaimFaucetQuery . data && canClaimFaucetQuery . data . canClaim === false ) {
187229 return (
@@ -250,3 +292,165 @@ export function FaucetButton({
250292 </ div >
251293 ) ;
252294}
295+
296+ const faucetFormSchema = z . object ( {
297+ amount : z . coerce . number ( ) . refine ( ( value ) => value > 0 , {
298+ message : "Amount must be greater than 0" ,
299+ } ) ,
300+ } ) ;
301+
302+ function SendFundsToFaucetModalButton ( props : {
303+ chain : Chain ;
304+ isLoggedIn : boolean ;
305+ client : ThirdwebClient ;
306+ chainMeta : ChainMetadata ;
307+ onFaucetRefill : ( ) => void ;
308+ } ) {
309+ return (
310+ < Dialog >
311+ < DialogTrigger asChild >
312+ < Button variant = "default" className = "w-full" >
313+ Refill Faucet
314+ </ Button >
315+ </ DialogTrigger >
316+ < DialogContent >
317+ < DialogHeader className = "mb-2" >
318+ < DialogTitle > Refill Faucet</ DialogTitle >
319+ < DialogDescription > Send funds to faucet wallet</ DialogDescription >
320+ </ DialogHeader >
321+
322+ < SendFundsToFaucetModalContent { ...props } />
323+ </ DialogContent >
324+ </ Dialog >
325+ ) ;
326+ }
327+
328+ function SendFundsToFaucetModalContent ( props : {
329+ chain : Chain ;
330+ isLoggedIn : boolean ;
331+ client : ThirdwebClient ;
332+ chainMeta : ChainMetadata ;
333+ onFaucetRefill : ( ) => void ;
334+ } ) {
335+ const account = useActiveAccount ( ) ;
336+ const activeChain = useActiveWalletChain ( ) ;
337+ const switchActiveWalletChain = useSwitchActiveWalletChain ( ) ;
338+ const sendTxMutation = useSendTransaction ( {
339+ payModal : false ,
340+ } ) ;
341+ const switchChainMutation = useMutation ( {
342+ mutationFn : async ( ) => {
343+ await switchActiveWalletChain ( props . chain ) ;
344+ } ,
345+ } ) ;
346+
347+ const form = useForm < z . infer < typeof faucetFormSchema > > ( {
348+ resolver : zodResolver ( faucetFormSchema ) ,
349+ defaultValues : {
350+ amount : 0.1 ,
351+ } ,
352+ } ) ;
353+
354+ function onSubmit ( values : z . infer < typeof faucetFormSchema > ) {
355+ const sendNativeTokenTx = prepareTransaction ( {
356+ chain : props . chain ,
357+ client : props . client ,
358+ to : THIRDWEB_ENGINE_FAUCET_WALLET ,
359+ value : toWei ( values . amount . toString ( ) ) ,
360+ } ) ;
361+
362+ const promise = sendTxMutation . mutateAsync ( sendNativeTokenTx ) ;
363+
364+ toast . promise ( promise , {
365+ success : `Sent ${ values . amount } ${ props . chainMeta . nativeCurrency . symbol } to faucet` ,
366+ error : `Failed to send ${ values . amount } ${ props . chainMeta . nativeCurrency . symbol } to faucet` ,
367+ } ) ;
368+
369+ promise . then ( ( ) => {
370+ props . onFaucetRefill ( ) ;
371+ } ) ;
372+ }
373+
374+ return (
375+ < Form { ...form } >
376+ < form
377+ onSubmit = { form . handleSubmit ( onSubmit ) }
378+ className = "flex min-w-0 flex-col gap-5"
379+ >
380+ < div className = "min-w-0" >
381+ < p className = "mb-2 text-foreground text-sm" > Faucet Wallet </ p >
382+ < CopyTextButton
383+ copyIconPosition = "right"
384+ variant = "outline"
385+ className = "w-full justify-between bg-card py-2 font-mono"
386+ textToCopy = { THIRDWEB_ENGINE_FAUCET_WALLET }
387+ textToShow = { THIRDWEB_ENGINE_FAUCET_WALLET }
388+ tooltip = { undefined }
389+ />
390+ </ div >
391+
392+ < FormField
393+ control = { form . control }
394+ name = "amount"
395+ render = { ( { field } ) => (
396+ < FormItem >
397+ < FormLabel > Amount</ FormLabel >
398+ < FormControl >
399+ < div className = "relative" >
400+ < Input
401+ { ...field }
402+ type = "number"
403+ className = "h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
404+ />
405+ < div className = "-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm" >
406+ { props . chainMeta . nativeCurrency . symbol }
407+ </ div >
408+ </ div >
409+ </ FormControl >
410+
411+ < FormMessage />
412+ </ FormItem >
413+ ) }
414+ />
415+
416+ { ! account && (
417+ < CustomConnectWallet
418+ chain = { props . chain }
419+ loginRequired = { false }
420+ isLoggedIn = { props . isLoggedIn }
421+ connectButtonClassName = "!w-full"
422+ detailsButtonClassName = "!w-full"
423+ />
424+ ) }
425+
426+ { account && activeChain && (
427+ < div >
428+ { activeChain . id === props . chain . id ? (
429+ < Button
430+ key = "submit"
431+ type = "submit"
432+ className = "mt-4 w-full gap-2"
433+ disabled = { sendTxMutation . isPending }
434+ >
435+ { sendTxMutation . isPending && < Spinner className = "size-4" /> }
436+ Send funds to faucet
437+ </ Button >
438+ ) : (
439+ < Button
440+ key = "switch"
441+ className = "mt-4 w-full gap-2"
442+ disabled = { switchChainMutation . isPending }
443+ onClick = { ( ) => switchChainMutation . mutate ( ) }
444+ >
445+ Switch to { props . chainMeta . name } { " " }
446+ { switchChainMutation . isPending && (
447+ < Spinner className = "size-4" />
448+ ) }
449+ </ Button >
450+ ) }
451+ </ div >
452+ ) }
453+ </ form >
454+ </ Form >
455+ ) ;
456+ }
0 commit comments