diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/BillingSettingsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/BillingSettingsPage.tsx index 9839a30451f..5a1d65e8d02 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/BillingSettingsPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/BillingSettingsPage.tsx @@ -4,7 +4,9 @@ import { Spinner } from "@/components/ui/Spinner/Spinner"; import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi"; import { Billing } from "components/settings/Account/Billing"; -export const SettingsBillingPage = () => { +export const SettingsBillingPage = (props: { + teamId: string | undefined; +}) => { const meQuery = useAccount({ refetchInterval: (query) => [ @@ -25,5 +27,5 @@ export const SettingsBillingPage = () => { ); } - return ; + return ; }; diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/page.tsx index 3ade1506093..6e66af03fa2 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/page.tsx @@ -1,10 +1,22 @@ +import { getTeamBySlug } from "@/api/team"; import { ChakraProviderSetup } from "@/components/ChakraProviderSetup"; +import { notFound } from "next/navigation"; import { SettingsBillingPage } from "./BillingSettingsPage"; -export default function Page() { +export default async function Page(props: { + params: { + team_slug: string; + }; +}) { + const team = await getTeamBySlug(props.params.team_slug); + + if (!team) { + notFound(); + } + return ( - + ); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.stories.tsx b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.stories.tsx new file mode 100644 index 00000000000..09323219145 --- /dev/null +++ b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Toaster } from "sonner"; +import { BadgeContainer, mobileViewport } from "../../../../stories/utils"; +import { CouponCardUI } from "./CouponCard"; + +const meta = { + title: "billing/CouponCard", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function statusStub(status: number) { + return async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return status; + }; +} + +function Story() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx new file mode 100644 index 00000000000..78f56031d15 --- /dev/null +++ b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +export function CouponCard(props: { + teamId: string | undefined; +}) { + return ( + { + const res = await fetch("/api/server-proxy/api/v1/coupons/redeem", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + promoCode, + teamId: props.teamId, + }), + }); + + return res.status; + }} + /> + ); +} + +const couponFormSchema = z.object({ + promoCode: z.string().min(1, "Coupon code is required"), +}); + +export function CouponCardUI(props: { + submit: (promoCode: string) => Promise; +}) { + const form = useForm>({ + resolver: zodResolver(couponFormSchema), + defaultValues: { + promoCode: "", + }, + }); + + const applyCoupon = useMutation({ + mutationFn: (promoCode: string) => props.submit(promoCode), + }); + + async function onSubmit(values: z.infer) { + try { + const status = await applyCoupon.mutateAsync(values.promoCode); + switch (status) { + case 200: { + toast.success("Coupon applied successfully"); + break; + } + case 400: { + toast.error("Coupon code is invalid"); + break; + } + case 401: { + toast.error("You are not authorized to apply coupons", { + description: "Login to dashboard and try again", + }); + break; + } + case 409: { + toast.error("Coupon already applied"); + break; + } + case 429: { + toast.error("Too many coupons applied in a short period", { + description: "Please try again after some time", + }); + break; + } + default: { + toast.error("Failed to apply coupon"); + } + } + } catch { + toast.error("Failed to apply coupon"); + } + + form.reset(); + } + + return ( +
+ {/* header */} +
+

+ Apply Coupon +

+

+ Enter your coupon code to apply discounts or free trials on thirdweb + products +

+
+ +
+ +
+ + {/* Body */} +
+ ( + + Coupon Code + + + + + + )} + /> +
+
+ + {/* Footer */} +
+ +
+ + +
+ ); +} diff --git a/apps/dashboard/src/components/settings/Account/Billing/index.tsx b/apps/dashboard/src/components/settings/Account/Billing/index.tsx index 7f3721b3692..4b202559d44 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/index.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/index.tsx @@ -18,6 +18,7 @@ import { FiExternalLink } from "react-icons/fi"; import { Button, Heading, Text, TrackedLink } from "tw-components"; import { PLANS } from "utils/pricing"; import { LazyOnboardingBilling } from "../../../onboarding/LazyOnboardingBilling"; +import { CouponCard } from "./CouponCard"; import { BillingDowngradeDialog } from "./DowngradeDialog"; import { BillingHeader } from "./Header"; import { BillingPlanCard } from "./PlanCard"; @@ -25,9 +26,10 @@ import { BillingPricing } from "./Pricing"; interface BillingProps { account: Account; + teamId: string | undefined; } -export const Billing: React.FC = ({ account }) => { +export const Billing: React.FC = ({ account, teamId }) => { const updatePlanMutation = useUpdateAccountPlan( account?.plan === AccountPlan.Free, ); @@ -301,6 +303,8 @@ export const Billing: React.FC = ({ account }) => { loading={updatePlanMutation.isPending} /> )} + + ); }; diff --git a/apps/dashboard/src/pages/dashboard/settings/billing.tsx b/apps/dashboard/src/pages/dashboard/settings/billing.tsx index a2ca1a75d06..d66d1099121 100644 --- a/apps/dashboard/src/pages/dashboard/settings/billing.tsx +++ b/apps/dashboard/src/pages/dashboard/settings/billing.tsx @@ -5,7 +5,7 @@ import type { ThirdwebNextPage } from "utils/types"; import { SettingsBillingPage } from "../../../app/team/[team_slug]/(team)/~/settings/billing/BillingSettingsPage"; const Page: ThirdwebNextPage = () => { - return ; + return ; }; Page.pageId = PageId.SettingsUsage;