From d261ef0d5982b73296dac8e00d27e15b782fedf6 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Tue, 29 Aug 2023 17:29:26 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Update=20billing=20page=20and=20?= =?UTF-8?q?refine=20ui=20components=20(#830)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/public/locales/en/app.json | 20 +- .../src/components/billing/billing-plans.tsx | 297 ++++++++++-------- apps/web/src/components/featurebase.tsx | 40 ++- .../components/layouts/standard-layout.tsx | 21 +- .../components/settings/settings-section.tsx | 4 +- apps/web/src/components/upgrade-button.tsx | 33 +- apps/web/src/components/user-dropdown.tsx | 19 +- apps/web/src/contexts/plan.tsx | 33 ++ apps/web/src/pages/settings/billing.tsx | 46 ++- packages/backend/trpc/routers/user.ts | 20 +- packages/backend/utils/auth.ts | 6 + packages/ui/button.tsx | 8 +- packages/ui/dialog.tsx | 10 +- packages/ui/tabs.tsx | 2 +- 14 files changed, 321 insertions(+), 238 deletions(-) diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 3c50e26e4..3098cfbb5 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -156,14 +156,11 @@ "goToInvite": "Go to Invite Page", "planPro": "Pro", "Billing": "Billing", - "annualBilling": "Annual billing (Save {discount}%)", "planUpgrade": "Upgrade", "subscriptionUpdatePayment": "Update Payment Details", "subscriptionCancel": "Cancel Subscription", "billingStatus": "Billing Status", "billingStatusDescription": "Manage your subscription and billing details", - "subscriptionPlans": "Plans", - "subscriptionDescription": "Get access to more features by upgrading to a paid plan.", "freeForever": "free forever", "annualBillingDescription": "per month, billed annually", "billingStatusState": "Status", @@ -176,12 +173,8 @@ "billingPeriod": "Period", "billingPeriodMonthly": "Monthly", "billingPeriodYearly": "Yearly", - "plan_unlimitedPolls": "Unlimited polls", - "plan_unlimitedParticipants": "Unlimited participants", "monthlyBillingDescription": "per month", - "plan_finalizePolls": "Finalize polls", "plan_extendedPollLife": "Keep polls indefinitely", - "plan_prioritySupport": "Priority support", "becomeATranslator": "Help translate", "noPolls": "No polls", "noPollsDescription": "Get started by creating a new poll.", @@ -200,7 +193,6 @@ "hideScoresDescription": "Only show scores until after a participant has voted", "disableComments": "Disable comments", "disableCommentsDescription": "Remove the option to leave a comment on the poll", - "planCustomizablePollSettings": "Customizable poll settings", "clockPreferences": "Clock Preferences", "clockPreferencesDescription": "Set your preferred time zone and time format.", "featureRequest": "Request a Feature", @@ -230,5 +222,15 @@ "billingPortal": "Billing Portal", "supportDescription": "Need help with anything?", "supportBilling": "Please reach out if you need any assistance.", - "contactSupport": "Contact Support" + "contactSupport": "Contact Support", + "planFreeDescription": "For casual users", + "currentPlan": "Current Plan", + "limitedAccess": "Access to core features", + "pollsDeleted": "Polls are automatically deleted once they become inactive", + "planProDescription": "For power users and professionals", + "accessAllFeatures": "Access all features", + "earlyAccess": "Get early access to new features", + "earlyAdopterDescription": "As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases.", + "nextPriceIncrease": "Next price increase is scheduled for {date}.", + "upgradeNowSaveLater": "Upgrade now, save later" } diff --git a/apps/web/src/components/billing/billing-plans.tsx b/apps/web/src/components/billing/billing-plans.tsx index eb4439b4a..a60d8f6d7 100644 --- a/apps/web/src/components/billing/billing-plans.tsx +++ b/apps/web/src/components/billing/billing-plans.tsx @@ -1,159 +1,184 @@ -import { - BillingPlan, - BillingPlanFooter, - BillingPlanHeader, - BillingPlanPeriod, - BillingPlanPerk, - BillingPlanPerks, - BillingPlanPrice, - BillingPlanTitle, -} from "@rallly/ui/billing-plan"; -import { Label } from "@rallly/ui/label"; -import { Switch } from "@rallly/ui/switch"; +import { CheckCircle2Icon, TrendingUpIcon } from "@rallly/icons"; +import { cn } from "@rallly/ui"; +import { BillingPlanPeriod, BillingPlanPrice } from "@rallly/ui/billing-plan"; +import { Button } from "@rallly/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs"; +import dayjs from "dayjs"; import React from "react"; import { Trans } from "@/components/trans"; import { UpgradeButton } from "@/components/upgrade-button"; -import { usePlan } from "@/contexts/plan"; const monthlyPriceUsd = 5; const annualPriceUsd = 30; +const Perks = ({ children }: React.PropsWithChildren) => { + return ; +}; + +const Perk = ({ + children, + pro, +}: React.PropsWithChildren<{ pro?: boolean }>) => { + return ( +
  • + +
    {children}
    +
  • + ); +}; + export const BillingPlans = () => { - const [isBilledAnnually, setBilledAnnually] = React.useState(true); - const plan = usePlan(); - const isPlus = plan === "paid"; + const [tab, setTab] = React.useState("yearly"); return (
    -
    - -

    + + + + + + + + + +

    +
    +
    +

    + +

    +

    + +

    +
    +
    + $0 + + + +
    +
    + + + + + + + + + +
    +
    +
    +

    + +

    +

    + +

    +
    +
    + + + ${monthlyPriceUsd} + + + + + + + ${monthlyPriceUsd} + + + + +
    +
    + + + + + + + + + + + + +
    +
    + +
    +
    + +
    +
    +

    + +

    +
    +

    -
    -
    - { - setBilledAnnually(checked); - }} - /> -
    -
    - - - - - - $0 - - - - - - - - - - - - - - - - {!isPlus ? ( - - - - ) : null} - +

    ); }; - -export const ProPlan = ({ - annual, - children, -}: React.PropsWithChildren<{ - annual?: boolean; -}>) => { - return ( - - - - - - {annual ? ( - <> - - ${monthlyPriceUsd} - - - - - - ) : ( - <> - ${monthlyPriceUsd} - - - - - )} - - - - - - - - - - - - - - - - - - - - - - {children} - - ); -}; diff --git a/apps/web/src/components/featurebase.tsx b/apps/web/src/components/featurebase.tsx index c65d606aa..e4c20cfa2 100644 --- a/apps/web/src/components/featurebase.tsx +++ b/apps/web/src/components/featurebase.tsx @@ -2,9 +2,11 @@ import { trpc } from "@rallly/backend"; import { HelpCircleIcon } from "@rallly/icons"; import { cn } from "@rallly/ui"; import { Button } from "@rallly/ui/button"; +import { TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; import Script from "next/script"; import React from "react"; +import Tooltip from "@/components/tooltip"; import { Trans } from "@/components/trans"; import { isFeedbackEnabled } from "@/utils/constants"; @@ -35,24 +37,28 @@ export const Changelog = ({ className }: { className?: string }) => { return ( <> - + + - - - + + ); }; diff --git a/apps/web/src/components/layouts/standard-layout.tsx b/apps/web/src/components/layouts/standard-layout.tsx index b094b6c64..44936a4b3 100644 --- a/apps/web/src/components/layouts/standard-layout.tsx +++ b/apps/web/src/components/layouts/standard-layout.tsx @@ -1,4 +1,4 @@ -import { ListIcon, LogInIcon } from "@rallly/icons"; +import { ListIcon, LogInIcon, SparklesIcon } from "@rallly/icons"; import { cn } from "@rallly/ui"; import { Button } from "@rallly/ui/button"; import clsx from "clsx"; @@ -16,6 +16,7 @@ import FeedbackButton from "@/components/feedback"; import { Spinner } from "@/components/spinner"; import { Trans } from "@/components/trans"; import { UserDropdown } from "@/components/user-dropdown"; +import { IfFreeUser } from "@/contexts/plan"; import { isFeedbackEnabled } from "@/utils/constants"; import { DayjsProvider } from "@/utils/dayjs"; @@ -56,6 +57,17 @@ const NavMenuItem = ({ ); }; +const Upgrade = () => { + return ( + + ); +}; + const Logo = () => { const router = useRouter(); const [isBusy, setIsBusy] = React.useState(false); @@ -106,7 +118,7 @@ const MainNav = () => { }} initial="hidden" animate="visible" - exit={"hidden"} + exit="hidden" className="border-b bg-gray-50/50" > @@ -121,7 +133,10 @@ const MainNav = () => {
    -
    diff --git a/apps/web/src/components/upgrade-button.tsx b/apps/web/src/components/upgrade-button.tsx index 5ac5cc4f1..849befd47 100644 --- a/apps/web/src/components/upgrade-button.tsx +++ b/apps/web/src/components/upgrade-button.tsx @@ -1,5 +1,6 @@ import { Button } from "@rallly/ui/button"; import Link from "next/link"; +import { Trans } from "next-i18next"; import React from "react"; import { usePostHog } from "@/utils/posthog"; @@ -11,23 +12,21 @@ export const UpgradeButton = ({ const posthog = usePostHog(); return ( - <> - - + {children || } + + ); }; diff --git a/apps/web/src/components/user-dropdown.tsx b/apps/web/src/components/user-dropdown.tsx index 46a44a9d4..658b86129 100644 --- a/apps/web/src/components/user-dropdown.tsx +++ b/apps/web/src/components/user-dropdown.tsx @@ -11,7 +11,6 @@ import { UserIcon, UserPlusIcon, } from "@rallly/icons"; -import { Badge } from "@rallly/ui/badge"; import { Button } from "@rallly/ui/button"; import { DropdownMenu, @@ -25,27 +24,11 @@ import Link from "next/link"; import { Trans } from "@/components/trans"; import { CurrentUserAvatar } from "@/components/user"; -import { usePlan } from "@/contexts/plan"; +import { Plan } from "@/contexts/plan"; import { isFeedbackEnabled } from "@/utils/constants"; import { IfAuthenticated, IfGuest, useUser } from "./user-provider"; -const Plan = () => { - const plan = usePlan(); - - if (plan === "paid") { - return ( - - - - ); - } - return ( - - - - ); -}; export const UserDropdown = () => { const { user } = useUser(); return ( diff --git a/apps/web/src/contexts/plan.tsx b/apps/web/src/contexts/plan.tsx index 9687cfde2..b24ed7ad3 100644 --- a/apps/web/src/contexts/plan.tsx +++ b/apps/web/src/contexts/plan.tsx @@ -1,4 +1,8 @@ import { trpc } from "@rallly/backend"; +import { Badge } from "@rallly/ui/badge"; +import React from "react"; + +import { Trans } from "@/components/trans"; export const usePlan = () => { const { data } = trpc.user.subscription.useQuery(); @@ -7,3 +11,32 @@ export const usePlan = () => { return isPaid ? "paid" : "free"; }; + +export const IfSubscribed = ({ children }: React.PropsWithChildren) => { + const plan = usePlan(); + + return plan === "paid" ? <>{children} : null; +}; + +export const IfFreeUser = ({ children }: React.PropsWithChildren) => { + const plan = usePlan(); + + return plan === "free" ? <>{children} : null; +}; + +export const Plan = () => { + const plan = usePlan(); + + if (plan === "paid") { + return ( + + + + ); + } + return ( + + + + ); +}; diff --git a/apps/web/src/pages/settings/billing.tsx b/apps/web/src/pages/settings/billing.tsx index 43b8a9aaa..56706947d 100644 --- a/apps/web/src/pages/settings/billing.tsx +++ b/apps/web/src/pages/settings/billing.tsx @@ -1,6 +1,5 @@ import { trpc } from "@rallly/backend"; import { ArrowUpRight, CreditCardIcon } from "@rallly/icons"; -import { Badge } from "@rallly/ui/badge"; import { Button } from "@rallly/ui/button"; import { Card } from "@rallly/ui/card"; import { Label } from "@rallly/ui/label"; @@ -14,6 +13,7 @@ import { getProfileLayout } from "@/components/layouts/profile-layout"; import { SettingsSection } from "@/components/settings/settings-section"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; +import { Plan } from "@/contexts/plan"; import { NextPageWithLayout } from "../../types"; import { getStaticTranslations } from "../../utils/with-page-translations"; @@ -28,12 +28,10 @@ declare global { const BillingPortal = () => { return (
    -
    - - - -
    -

    + +

    { return <>You need to be logged in.; } - if (data.legacy) { - // User is on the old billing system - return ; - } else if (data.active) { - return ; - } else { - return ; - } + return ( +

    +
    + +
    + +
    +
    + {!data.active ? ( +
    + + +
    + ) : data.legacy ? ( + + ) : ( + + )} +
    + ); }; const LegacyBilling = () => { @@ -229,7 +243,7 @@ const Page: NextPageWithLayout = () => { } >
    -

    +

    { - if (ctx.user.isGuest) { - // guest user can't have an active subscription - return { - active: false, - }; - } + subscription: possiblyPublicProcedure.query( + async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => { + if (ctx.user.isGuest) { + // guest user can't have an active subscription + return { + active: false, + }; + } - return await getSubscriptionStatus(ctx.user.id); - }), + return await getSubscriptionStatus(ctx.user.id); + }, + ), changeName: privateProcedure .input( z.object({ diff --git a/packages/backend/utils/auth.ts b/packages/backend/utils/auth.ts index 0327460e0..4f838d1fa 100644 --- a/packages/backend/utils/auth.ts +++ b/packages/backend/utils/auth.ts @@ -9,6 +9,7 @@ export const getSubscriptionStatus = async (userId: string) => { subscription: { select: { active: true, + periodEnd: true, }, }, }, @@ -17,6 +18,7 @@ export const getSubscriptionStatus = async (userId: string) => { if (user?.subscription?.active === true) { return { active: true, + expiresAt: user.subscription.periodEnd, }; } @@ -27,6 +29,9 @@ export const getSubscriptionStatus = async (userId: string) => { gt: new Date(), }, }, + select: { + endDate: true, + }, }); if ( @@ -36,6 +41,7 @@ export const getSubscriptionStatus = async (userId: string) => { return { active: true, legacy: true, + expiresAt: userPaymentData.endDate, }; } diff --git a/packages/ui/button.tsx b/packages/ui/button.tsx index 60e0ab518..e4648ceee 100644 --- a/packages/ui/button.tsx +++ b/packages/ui/button.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { cn } from "./lib/utils"; const buttonVariants = cva( - "inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-200 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center gap-x-2 whitespace-nowrap rounded-md border", + "inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-200 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border", { variants: { variant: { @@ -22,8 +22,8 @@ const buttonVariants = cva( link: "underline-offset-4 hover:underline text-primary", }, size: { - default: "h-9 px-2.5 text-sm", - sm: "h-7 text-xs px-1.5 rounded-md", + default: "h-9 px-2.5 gap-x-2.5 text-sm", + sm: "h-7 text-xs px-2 gap-x-1.5 rounded-md", lg: "h-11 text-base px-4 rounded-md", }, }, @@ -78,7 +78,7 @@ const Button = React.forwardRef( {loading ? ( ) : Icon ? ( - + ) : null} {children} diff --git a/packages/ui/dialog.tsx b/packages/ui/dialog.tsx index e652bff7a..ea22f902a 100644 --- a/packages/ui/dialog.tsx +++ b/packages/ui/dialog.tsx @@ -51,7 +51,7 @@ const DialogContent = React.forwardRef< {children} - - {!hideCloseButton ? ( + {!hideCloseButton ? ( + <> Close - ) : null} - + + ) : null} )); diff --git a/packages/ui/tabs.tsx b/packages/ui/tabs.tsx index 3160d8ae0..eb528ae56 100644 --- a/packages/ui/tabs.tsx +++ b/packages/ui/tabs.tsx @@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<