Skip to content

Commit 7eaa60a

Browse files
committed
update
1 parent f40d247 commit 7eaa60a

File tree

4 files changed

+152
-39
lines changed

4 files changed

+152
-39
lines changed

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

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
34
import { Spinner } from "@/components/ui/Spinner/Spinner";
45
import { Button } from "@/components/ui/button";
56
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
@@ -9,11 +10,14 @@ import {
910
} from "@/constants/env";
1011
import { useThirdwebClient } from "@/constants/thirdweb.client";
1112
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
13+
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
1214
import { Turnstile } from "@marsidev/react-turnstile";
1315
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1416
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
17+
import { Onboarding } from "components/onboarding";
1518
import { mapV4ChainToV5Chain } from "contexts/map-chains";
1619
import { useTrack } from "hooks/analytics/useTrack";
20+
import { useState } from "react";
1721
import { useForm } from "react-hook-form";
1822
import { toast } from "sonner";
1923
import { toUnits } from "thirdweb";
@@ -133,50 +137,82 @@ export function FaucetButton({
133137

134138
const form = useForm<z.infer<typeof claimFaucetSchema>>();
135139

140+
const accountQuery = useAccount();
141+
const [showOnboarding, setShowOnBoarding] = useState(false);
142+
136143
// loading state
137-
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
144+
if (
145+
faucetWalletBalanceQuery.isPending ||
146+
canClaimFaucetQuery.isPending ||
147+
accountQuery.isLoading
148+
) {
138149
return (
139150
<Button variant="outline" className="w-full gap-2">
140151
Checking Faucet <Spinner className="size-3" />
141152
</Button>
142153
);
143154
}
144155

145-
// faucet is empty
146-
if (isFaucetEmpty) {
147-
return (
148-
<Button variant="outline" disabled className="!opacity-100 w-full ">
149-
Faucet is empty right now
150-
</Button>
151-
);
152-
}
156+
// // faucet is empty
157+
// if (isFaucetEmpty) {
158+
// return (
159+
// <Button variant="outline" disabled className="!opacity-100 w-full">
160+
// Faucet is empty right now
161+
// </Button>
162+
// );
163+
// }
153164

154-
// Can not claim
155-
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
156-
return (
157-
<Button variant="outline" className="!opacity-100 w-full " disabled>
158-
{canClaimFaucetQuery.data.type === "throttle" && (
159-
<>
160-
Your next claim is available{" "}
161-
{formatTime(canClaimFaucetQuery.data.ttlSeconds)}
162-
</>
163-
)}
165+
// // Can not claim
166+
// if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
167+
// return (
168+
// <Button variant="outline" className="!opacity-100 w-full " disabled>
169+
// {canClaimFaucetQuery.data.type === "throttle" && (
170+
// <>
171+
// Your next claim is available{" "}
172+
// {formatTime(canClaimFaucetQuery.data.ttlSeconds)}
173+
// </>
174+
// )}
164175

165-
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
166-
"Faucet is empty right now"}
167-
</Button>
168-
);
169-
}
176+
// {canClaimFaucetQuery.data.type === "unsupported-chain" &&
177+
// "Faucet is empty right now"}
178+
// </Button>
179+
// );
180+
// }
170181

171-
if (!address) {
182+
if (!address || !accountQuery.data) {
172183
return (
173184
<CustomConnectWallet
174-
loginRequired={false}
185+
loginRequired={true}
175186
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
176187
/>
177188
);
178189
}
179190

191+
// Email verification is required to claim from the faucet
192+
// same logic with the Onboarding state
193+
if (
194+
!accountQuery.data.emailConfirmedAt &&
195+
!accountQuery.data.unconfirmedEmail
196+
) {
197+
return (
198+
<>
199+
<Button
200+
variant="outline"
201+
className="!opacity-100 w-full"
202+
onClick={() => setShowOnBoarding(true)}
203+
>
204+
Verify your email
205+
</Button>
206+
{/* We will show the modal only if the user click on it, because this is a public page */}
207+
{showOnboarding && (
208+
<ChakraProviderSetup>
209+
<Onboarding isClosable={true} onOpenChange={setShowOnBoarding} />
210+
</ChakraProviderSetup>
211+
)}
212+
</>
213+
);
214+
}
215+
180216
const claimFunds = (values: z.infer<typeof claimFaucetSchema>) => {
181217
// Instead of having a dedicated endpoint (/api/verify-token),
182218
// we can just attach the token in the payload and send it to the claim-faucet endpoint, to avoid a round-trip request

apps/dashboard/src/app/api/testnet-faucet/claim/route.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
2+
import { API_SERVER_URL } from "@/constants/env";
3+
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
14
import { startOfToday } from "date-fns";
25
import { cacheGet, cacheSet } from "lib/redis";
36
import { type NextRequest, NextResponse } from "next/server";
4-
import { ZERO_ADDRESS } from "thirdweb";
7+
import { ZERO_ADDRESS, getAddress } from "thirdweb";
58
import { getFaucetClaimAmount } from "./claim-amount";
69

710
const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL;
@@ -19,6 +22,60 @@ interface RequestTestnetFundsPayload {
1922

2023
// Note: This handler cannot use "edge" runtime because of Redis usage.
2124
export const POST = async (req: NextRequest) => {
25+
// Make sure user's connected to the site
26+
const activeAccount = req.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value;
27+
28+
if (!activeAccount) {
29+
return NextResponse.json(
30+
{
31+
error: "No wallet detected",
32+
},
33+
{ status: 400 },
34+
);
35+
}
36+
const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(activeAccount);
37+
38+
const authCookie = req.cookies.get(authCookieName);
39+
40+
if (!authCookie) {
41+
return NextResponse.json(
42+
{
43+
error: "No wallet connected",
44+
},
45+
{ status: 400 },
46+
);
47+
}
48+
49+
// Make sure the connected wallet has a thirdweb account
50+
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
51+
method: "GET",
52+
headers: {
53+
Authorization: `Bearer ${authCookie.value}`,
54+
},
55+
});
56+
57+
if (accountRes.status !== 200) {
58+
// Account not found on this connected address
59+
return NextResponse.json(
60+
{
61+
error: "thirdweb account not found",
62+
},
63+
{ status: 400 },
64+
);
65+
}
66+
67+
const account: Account = await accountRes.json();
68+
69+
// Make sure the logged-in account has verified its email
70+
if (!account.emailConfirmedAt && !account.unconfirmedEmail) {
71+
return NextResponse.json(
72+
{
73+
error: "Account owner hasn't verified email",
74+
},
75+
{ status: 400 },
76+
);
77+
}
78+
2279
const requestBody = (await req.json()) as RequestTestnetFundsPayload;
2380
const { chainId, toAddress, turnstileToken } = requestBody;
2481
if (Number.isNaN(chainId)) {
@@ -86,17 +143,17 @@ export const POST = async (req: NextRequest) => {
86143
);
87144
}
88145

89-
const ipCacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
90146
const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`;
147+
const accountCacheKey = `testnet-faucet:${chainId}:${account.id}`;
91148

92-
// Assert 1 request per IP/chain every 24 hours.
149+
// Assert 1 request per userId every 24 hours.
93150
// get the cached value
94-
const [ipCacheValue, addressCache] = await Promise.all([
95-
cacheGet(ipCacheKey),
151+
const [accountCacheValue, addressCache] = await Promise.all([
152+
cacheGet(accountCacheKey),
96153
cacheGet(addressCacheKey),
97154
]);
98155
// if we have a cached value, return an error
99-
if (ipCacheValue !== null || addressCache !== null) {
156+
if (accountCacheValue !== null || addressCache !== null) {
100157
return NextResponse.json(
101158
{ error: "Already requested funds on this chain in the past 24 hours." },
102159
{ status: 429 },
@@ -109,13 +166,13 @@ export const POST = async (req: NextRequest) => {
109166
todayLocal.getTime() - todayLocal.getTimezoneOffset() * 60000,
110167
);
111168
const todayUTCSeconds = Math.floor(todayUTC.getTime() / 1000);
112-
const idempotencyKey = `${ipCacheKey}:${todayUTCSeconds}`;
169+
const idempotencyKey = `${addressCacheKey}:${todayUTCSeconds}`;
113170
const amountToClaim = getFaucetClaimAmount(chainId).toString();
114171

115172
try {
116173
// Store the claim request for 24 hours.
117174
await Promise.all([
118-
cacheSet(ipCacheKey, "claimed", 24 * 60 * 60),
175+
cacheSet(accountCacheKey, "claimed", 24 * 60 * 60),
119176
cacheSet(addressCacheKey, "claimed", 24 * 60 * 60),
120177
]);
121178
// then actually transfer the funds

apps/dashboard/src/components/onboarding/Modal.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
import { Dialog, DialogContent } from "@/components/ui/dialog";
22
import { cn } from "@/lib/utils";
33
import { IconLogo } from "components/logo";
4+
import type { Dispatch, SetStateAction } from "react";
45
import type { ComponentWithChildren } from "types/component-with-children";
56

67
interface OnboardingModalProps {
78
isOpen: boolean;
89
wide?: boolean;
10+
isClosable?: boolean;
11+
onOpenChange?: Dispatch<SetStateAction<boolean>>;
912
}
1013

1114
export const OnboardingModal: ComponentWithChildren<OnboardingModalProps> = ({
1215
children,
1316
isOpen,
1417
wide,
18+
isClosable,
19+
onOpenChange,
1520
}) => {
1621
return (
17-
<Dialog open={isOpen}>
22+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
1823
<DialogContent
1924
dialogOverlayClassName="z-[10000]"
2025
className={cn("z-[10001] max-h-[90vh] overflow-auto", {
2126
"!max-w-[768px]": wide,
2227
})}
23-
dialogCloseClassName="hidden"
28+
dialogCloseClassName={isClosable ? "" : "hidden"}
2429
>
2530
<div className="flex flex-col gap-4">
2631
<div className="w-[40px]">

apps/dashboard/src/components/onboarding/index.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import {
55
useAccount,
66
} from "@3rdweb-sdk/react/hooks/useApi";
77
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
8-
import { Suspense, lazy, useEffect, useState } from "react";
8+
import {
9+
type Dispatch,
10+
type SetStateAction,
11+
Suspense,
12+
lazy,
13+
useEffect,
14+
useState,
15+
} from "react";
916
import { useActiveWallet } from "thirdweb/react";
1017
import { useTrack } from "../../hooks/analytics/useTrack";
1118
import { LazyOnboardingBilling } from "./LazyOnboardingBilling";
@@ -42,7 +49,10 @@ type OnboardingState =
4249
| "skipped"
4350
| undefined;
4451

45-
export const Onboarding: React.FC = () => {
52+
export const Onboarding: React.FC<{
53+
isClosable?: boolean;
54+
onOpenChange?: Dispatch<SetStateAction<boolean>>;
55+
}> = ({ isClosable, onOpenChange }) => {
4656
const meQuery = useAccount();
4757

4858
const { isLoggedIn } = useLoggedInUser();
@@ -202,7 +212,12 @@ export const Onboarding: React.FC = () => {
202212
}
203213

204214
return (
205-
<OnboardingModal isOpen={!!state} wide={state === "plan"}>
215+
<OnboardingModal
216+
isOpen={!!state}
217+
wide={state === "plan"}
218+
isClosable={isClosable}
219+
onOpenChange={onOpenChange}
220+
>
206221
{state === "onboarding" && (
207222
<Suspense fallback={<Loading />}>
208223
<OnboardingGeneral

0 commit comments

Comments
 (0)