diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx index 48264ebd066..5645b994244 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx @@ -237,6 +237,13 @@ export function FaucetButton({ {canClaimFaucetQuery.data.type === "unsupported-chain" && "Faucet is empty right now"} + + {canClaimFaucetQuery.data.type === "paid-plan-required" && ( + <> + This faucet is temporarily only available to Growth and{" "} + Pro customers. + + )} ); } diff --git a/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts b/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts index 28616e54f33..882cb0031e0 100644 --- a/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts +++ b/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts @@ -7,4 +7,8 @@ export type CanClaimResponseType = | { canClaim: false; type: "unsupported-chain"; + } + | { + canClaim: false; + type: "paid-plan-required"; }; diff --git a/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts b/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts index 503779977b0..937f7b1de13 100644 --- a/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts +++ b/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts @@ -1,3 +1,4 @@ +import { getTeams } from "@/api/team"; import { DISABLE_FAUCET_CHAIN_IDS, THIRDWEB_ACCESS_TOKEN, @@ -8,6 +9,7 @@ import { ipAddress } from "@vercel/functions"; import { cacheTtl } from "lib/redis"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +import { FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID } from "../config"; import type { CanClaimResponseType } from "./CanClaimResponseType"; // Note: This handler cannot use "edge" runtime because of Redis usage. @@ -46,6 +48,30 @@ export const GET = async (req: NextRequest) => { } catch {} } + // IF the faucet requires a paid plan, check if the user has a paid plan + if (FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID.has(chainId)) { + // get the teams for the account + const teams = await getTeams(); + if (!teams) { + const res: CanClaimResponseType = { + canClaim: false, + type: "paid-plan-required", + }; + return NextResponse.json(res); + } + // check if ANY of the customer's teams has "growth" or "pro" plan + const hasPaidPlan = teams.some((team) => + ["growth", "pro"].includes(team.billingPlan), + ); + if (!hasPaidPlan) { + const res: CanClaimResponseType = { + canClaim: false, + type: "paid-plan-required", + }; + return NextResponse.json(res); + } + } + if ( !THIRDWEB_ENGINE_URL || !THIRDWEB_ENGINE_FAUCET_WALLET || diff --git a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts index f2d7d0d6e87..5497fc025ac 100644 --- a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts +++ b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts @@ -1,3 +1,4 @@ +import { getTeams } from "@/api/team"; import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; import { API_SERVER_URL, @@ -11,6 +12,7 @@ import { startOfToday } from "date-fns"; import { cacheGet, cacheSet } from "lib/redis"; import { type NextRequest, NextResponse } from "next/server"; import { ZERO_ADDRESS, getAddress } from "thirdweb"; +import { FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID } from "../config"; import { getFaucetClaimAmount } from "./claim-amount"; interface RequestTestnetFundsPayload { @@ -53,36 +55,6 @@ export const POST = async (req: NextRequest) => { ); } - // Make sure the connected wallet has a thirdweb account - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${authCookie.value}`, - }, - }); - - if (accountRes.status !== 200) { - // Account not found on this connected address - return NextResponse.json( - { - error: "thirdweb account not found", - }, - { status: 400 }, - ); - } - - const account: { data: Account } = await accountRes.json(); - - // Make sure the logged-in account has verified its email - if (!account.data.email) { - return NextResponse.json( - { - error: "Account owner hasn't verified email", - }, - { status: 400 }, - ); - } - const requestBody = (await req.json()) as RequestTestnetFundsPayload; const { chainId, toAddress, turnstileToken } = requestBody; if (Number.isNaN(chainId)) { @@ -150,6 +122,66 @@ export const POST = async (req: NextRequest) => { ); } + // Make sure the connected wallet has a thirdweb account + const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { + method: "GET", + headers: { + Authorization: `Bearer ${authCookie.value}`, + }, + }); + + if (accountRes.status !== 200) { + // Account not found on this connected address + return NextResponse.json( + { + error: "thirdweb account not found", + }, + { status: 400 }, + ); + } + + const account: { data: Account } = await accountRes.json(); + + // Make sure the logged-in account has verified its email + if (!account.data.email) { + return NextResponse.json( + { + error: "Account owner hasn't verified email", + }, + { status: 400 }, + ); + } + + // IF the faucet requires a paid plan, check if the user has a paid plan + if (FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID.has(chainId)) { + // get the teams for the account + const teams = await getTeams(); + if (!teams) { + return NextResponse.json( + { + error: "No teams found for this account.", + }, + { + status: 500, + }, + ); + } + // check if ANY of the customer's teams has "growth" or "pro" plan + const hasPaidPlan = teams.some((team) => + ["growth", "pro"].includes(team.billingPlan), + ); + if (!hasPaidPlan) { + return NextResponse.json( + { + error: "Paid plan required to claim on this chain.", + }, + { + status: 402, + }, + ); + } + } + const ipCacheKey = `testnet-faucet:${chainId}:${ip}`; const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`; const accountCacheKey = `testnet-faucet:${chainId}:${account.data.id}`; diff --git a/apps/dashboard/src/app/api/testnet-faucet/config.ts b/apps/dashboard/src/app/api/testnet-faucet/config.ts new file mode 100644 index 00000000000..abdceb3b3d4 --- /dev/null +++ b/apps/dashboard/src/app/api/testnet-faucet/config.ts @@ -0,0 +1,3 @@ +export const FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID = new Set([ + 10143, // monad testnet +]);