Skip to content

Commit 00ab0e5

Browse files
committed
[TOOL-3216] Dashboard: Add Facuet Refill option on testnet chain pages
1 parent 08cc489 commit 00ab0e5

File tree

4 files changed

+236
-26
lines changed

4 files changed

+236
-26
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx

Lines changed: 226 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
"use client";
2+
import { CopyTextButton } from "@/components/ui/CopyTextButton";
23
import { Spinner } from "@/components/ui/Spinner/Spinner";
34
import { 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";
522
import {
623
THIRDWEB_ENGINE_FAUCET_WALLET,
724
TURNSTILE_SITE_KEY,
825
} from "@/constants/env";
926
import { useThirdwebClient } from "@/constants/thirdweb.client";
27+
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
1028
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
29+
import { zodResolver } from "@hookform/resolvers/zod";
1130
import { Turnstile } from "@marsidev/react-turnstile";
1231
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1332
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
@@ -17,9 +36,21 @@ import Link from "next/link";
1736
import { usePathname } from "next/navigation";
1837
import { useForm } from "react-hook-form";
1938
import { toast } from "sonner";
20-
import { toUnits } from "thirdweb";
39+
import {
40+
type ThirdwebClient,
41+
prepareTransaction,
42+
toUnits,
43+
toWei,
44+
} from "thirdweb";
2145
import 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";
2354
import { z } from "zod";
2455
import { 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,166 @@ 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+
// 1. Define your form.
348+
const form = useForm<z.infer<typeof faucetFormSchema>>({
349+
resolver: zodResolver(faucetFormSchema),
350+
defaultValues: {
351+
amount: 0.1,
352+
},
353+
});
354+
355+
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
356+
const sendNativeTokenTx = prepareTransaction({
357+
chain: props.chain,
358+
client: props.client,
359+
to: THIRDWEB_ENGINE_FAUCET_WALLET,
360+
value: toWei(values.amount.toString()),
361+
});
362+
363+
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
364+
365+
toast.promise(promise, {
366+
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
367+
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
368+
});
369+
370+
promise.then(() => {
371+
props.onFaucetRefill();
372+
});
373+
}
374+
375+
return (
376+
<Form {...form}>
377+
<form
378+
onSubmit={form.handleSubmit(onSubmit)}
379+
className="flex min-w-0 flex-col gap-5"
380+
>
381+
<div className="min-w-0">
382+
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
383+
<CopyTextButton
384+
copyIconPosition="right"
385+
variant="outline"
386+
className="w-full justify-between bg-card py-2 font-mono"
387+
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
388+
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
389+
tooltip={undefined}
390+
/>
391+
</div>
392+
393+
<FormField
394+
control={form.control}
395+
name="amount"
396+
render={({ field }) => (
397+
<FormItem>
398+
<FormLabel>Amount</FormLabel>
399+
<FormControl>
400+
<div className="relative">
401+
<Input
402+
{...field}
403+
type="number"
404+
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
405+
/>
406+
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
407+
{props.chainMeta.nativeCurrency.symbol}
408+
</div>
409+
</div>
410+
</FormControl>
411+
412+
<FormMessage />
413+
</FormItem>
414+
)}
415+
/>
416+
417+
{!account && (
418+
<CustomConnectWallet
419+
chain={props.chain}
420+
loginRequired={false}
421+
isLoggedIn={props.isLoggedIn}
422+
connectButtonClassName="!w-full"
423+
detailsButtonClassName="!w-full"
424+
/>
425+
)}
426+
427+
{account && activeChain && (
428+
<div>
429+
{activeChain.id === props.chain.id ? (
430+
<Button
431+
key="submit"
432+
type="submit"
433+
className="mt-4 w-full gap-2"
434+
disabled={sendTxMutation.isPending}
435+
>
436+
{sendTxMutation.isPending && <Spinner className="size-4" />}
437+
Send funds to faucet
438+
</Button>
439+
) : (
440+
<Button
441+
key="switch"
442+
className="mt-4 w-full gap-2"
443+
disabled={switchChainMutation.isPending}
444+
onClick={() => switchChainMutation.mutate()}
445+
>
446+
Switch to {props.chainMeta.name}{" "}
447+
{switchChainMutation.isPending && (
448+
<Spinner className="size-4" />
449+
)}
450+
</Button>
451+
)}
452+
</div>
453+
)}
454+
</form>
455+
</Form>
456+
);
457+
}

apps/dashboard/src/app/components/sdk-component-theme.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
2525
accentText: "hsl(var(--link-foreground))",
2626
accentButtonBg: "hsl(var(--primary))",
2727
accentButtonText: "hsl(var(--primary-foreground))",
28-
primaryButtonBg: "hsl(var(--background))",
29-
primaryButtonText: "hsl(var(--foreground))",
28+
primaryButtonBg: "hsl(var(--inverted))",
29+
primaryButtonText: "hsl(var(--inverted-foreground))",
3030
secondaryButtonText: "hsl(var(--secondary-foreground))",
3131
tooltipBg: "hsl(var(--popover))",
3232
tooltipText: "hsl(var(--popover-foreground))",

apps/dashboard/src/app/layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import "@/styles/globals.css";
2-
import { Toaster } from "@/components/ui/sonner";
32
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
43
import { cn } from "@/lib/utils";
54
import type { Metadata } from "next";
@@ -82,7 +81,6 @@ export default function RootLayout({
8281
<EnsureValidConnectedWalletLoginServer />
8382
</Suspense>
8483
</AppRouterProviders>
85-
<Toaster richColors />
8684
<DashboardRouterTopProgressBar />
8785
<NextTopLoader
8886
color="hsl(var(--primary))"

apps/dashboard/src/app/providers.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22

33
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4-
import { ThemeProvider } from "next-themes";
4+
import { ThemeProvider, useTheme } from "next-themes";
55
import { useEffect, useMemo } from "react";
6+
import { Toaster } from "sonner";
67
import {
78
ThirdwebProvider,
89
useActiveAccount,
@@ -31,6 +32,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
3132
enableSystem={false}
3233
defaultTheme="dark"
3334
>
35+
<ToasterSetup />
3436
<SanctionedAddressesChecker>
3537
{props.children}
3638
</SanctionedAddressesChecker>
@@ -40,6 +42,11 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
4042
);
4143
}
4244

45+
function ToasterSetup() {
46+
const { theme } = useTheme();
47+
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
48+
}
49+
4350
function SyncChainDefinitionsToConnectionManager() {
4451
const { allChainsV5 } = useAllChainsData();
4552
const connectionManager = useConnectionManager();

0 commit comments

Comments
 (0)