From df128bfc17160a9e38f33c63958e9c11142f4175 Mon Sep 17 00:00:00 2001 From: MananTank Date: Sat, 19 Oct 2024 18:31:07 +0000 Subject: [PATCH] Open Share Modal after applying freewallets coupon (#5085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem solved Short description of the bug fixed or feature added --- ## PR-Codex overview This PR introduces a new modal component, `ShareFreeWalletsModal`, to facilitate sharing free wallet offers via social media or email. It also updates the `CouponCard` to handle coupon applications and trigger the modal. ### Detailed summary - Added `ShareFreeWalletsModal` component for sharing options. - Created Storybook stories for `ShareFreeWalletsModal` in `share-free-wallets-modal.stories.tsx`. - Updated `ApplyCouponCard` to include an additional parameter for coupon application callback. - Integrated lazy loading for `ShareFreeWalletsModal` in `CouponCard`. - Implemented state management for modal visibility in `CouponSection`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../settings/Account/Billing/CouponCard.tsx | 96 +++-- .../share-free-wallets-modal.client.tsx | 404 ++++++++++++++++++ .../share-free-wallets-modal.stories.tsx | 37 ++ 3 files changed, 506 insertions(+), 31 deletions(-) create mode 100644 apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.client.tsx create mode 100644 apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.stories.tsx diff --git a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx index 44fb4f82a74..93dcf204875 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/CouponCard.tsx @@ -16,12 +16,24 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery } from "@tanstack/react-query"; import { format, fromUnixTime } from "date-fns"; import { TagIcon } from "lucide-react"; +import dynamic from "next/dynamic"; import { useSearchParams } from "next/navigation"; import { Suspense, useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +const LazyShareFreeWalletsModal = dynamic( + () => + import("./share-free-wallets-modal.client").then( + (mod) => mod.ShareFreeWalletsModal, + ), + { + ssr: false, + loading: () => null, + }, +); + export type ActiveCouponResponse = { id: string; start: number; @@ -36,7 +48,10 @@ export type ActiveCouponResponse = { function ApplyCouponCard(props: { teamId: string | undefined; - onCouponApplied: (data: ActiveCouponResponse) => void; + onCouponApplied: ( + data: ActiveCouponResponse, + isFreeWalletsCoupon: boolean, + ) => void; isPaymentSetup: boolean; onAddPayment: () => void; }) { @@ -87,7 +102,9 @@ export function ApplyCouponCardUI(props: { status: number; data: null | ActiveCouponResponse; }>; - onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined; + onCouponApplied: + | ((data: ActiveCouponResponse, isFreeWalletsCoupon: boolean) => void) + | undefined; prefillPromoCode?: string; scrollIntoView?: boolean; isPaymentSetup: boolean; @@ -130,7 +147,12 @@ export function ApplyCouponCardUI(props: { case 200: { toast.success("Coupon applied successfully"); if (res.data) { - props.onCouponApplied?.(res.data); + props.onCouponApplied?.( + res.data, + // prod & dev + values.promoCode === "FREEWALLETS" || + values.promoCode === "TESTFREEWALLETS", + ); } break; } @@ -258,6 +280,7 @@ export function CouponSection(props: { isPaymentSetup: boolean; onAddPayment: () => void; }) { + const [showShareModal, setShowShareModal] = useState(false); const loggedInUser = useLoggedInUser(); const [optimisticCouponData, setOptimisticCouponData] = useState< | { @@ -319,35 +342,46 @@ export function CouponSection(props: { ? optimisticCouponData.data : activeCoupon.data; - if (couponData) { - return ( - - ); - } - return ( - }> - { - setOptimisticCouponData({ - type: "added", - data: coupon, - }); - activeCoupon.refetch().then(() => { - setOptimisticCouponData(undefined); - }); - }} - isPaymentSetup={props.isPaymentSetup} - onAddPayment={props.onAddPayment} - /> - + <> + {couponData ? ( + + ) : ( + }> + { + setOptimisticCouponData({ + type: "added", + data: coupon, + }); + + if (isFreeWalletsCoupon) { + setShowShareModal(true); + } + activeCoupon.refetch().then(() => { + setOptimisticCouponData(undefined); + }); + }} + isPaymentSetup={props.isPaymentSetup} + onAddPayment={props.onAddPayment} + /> + + )} + + {showShareModal && ( + + )} + ); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.client.tsx b/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.client.tsx new file mode 100644 index 00000000000..bb5a6e98b5d --- /dev/null +++ b/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.client.tsx @@ -0,0 +1,404 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ChevronLeftIcon, MailIcon } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { DynamicHeight } from "../../../../@/components/ui/DynamicHeight"; +import { XIcon } from "../../../icons/brand-icons/XIcon"; + +export function ShareFreeWalletsModal(props: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; +}) { + const [screen, setScreen] = useState<"base" | "email">("base"); + + return ( + + + + {screen === "base" && ( +
+ +
+ +
+ + Congratulations! + + + You are getting free wallets for the next 12 months + +
+
+ + +
+
+ )} + + {screen === "email" && ( + setScreen("base")} /> + )} +
+
+
+ ); +} + +const emailFormSchema = z.object({ + email: z.string().email(), + app: z.string(), +}); + +function SendEmailScreen(props: { + goBack: () => void; +}) { + const form = useForm>({ + resolver: zodResolver(emailFormSchema), + defaultValues: { + email: "", + app: "default", + }, + }); + + return ( +
+
+ {})}> +
+ +
+ +
+ + Share via Email + + + Give the gift of unlimited wallets to another dev + +
+ +
+ + ( + + Email + + + + + + + )} + /> + +
+ + ( + + Share with + + + + + )} + /> +
+ +
+ + + {!form.formState.isValid ? ( + + ) : ( + + )} +
+ + +
+ ); +} + +function openDefaultEmailApp(email: string) { + return `mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`; +} + +function openGmailEmailApp(email: string) { + return `https://mail.google.com/mail/?view=cm&fs=1&to=${email}&su=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`; +} + +const twitterPostBody = `\ +Just claimed the free, unlimited wallets offer from @thirdweb! 🚀 + +No more per-wallet pricing, full support for web, mobile, and console. + +Here's to building without constraints ✨ + +👉 https://thirdweb.com/unlimited-wallets 👈`; + +const emailSubject = "You've unlocked Unlimited Wallets!"; + +const emailPostBody = `\ +Hey there, + +I just came across something that might interest you. thirdweb is offering free, unlimited wallets for developers. +I've claimed it and thought you might want to take a look too. + +Here's the lowdown: +* It's completely free +* No more per-wallet pricing constraints +* Full support for web, mobile, and console development + +I'm pretty excited about the possibilities this opens up for building without limitations. You can check it out here: https://thirdweb.com/unlimited-wallets + +Let me know what you think if you decide to give it a try!`; + +function VibrantSmileIcon(props: { + className?: string; +}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function VibrantEmailIcon(props: { + className?: string; +}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.stories.tsx b/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.stories.tsx new file mode 100644 index 00000000000..5a23e97d2f9 --- /dev/null +++ b/apps/dashboard/src/components/settings/Account/Billing/share-free-wallets-modal.stories.tsx @@ -0,0 +1,37 @@ +import { Button } from "@/components/ui/button"; +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { mobileViewport } from "../../../../stories/utils"; +import { ShareFreeWalletsModal } from "./share-free-wallets-modal.client"; + +const meta = { + title: "Billing/coupons/ShareFreeWalletsModal", + component: Variants, + parameters: { + layout: "centered", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Variants() { + const [isOpen, setIsOpen] = useState(true); + return ( +
+ + +
+ ); +}