💄 Update billing page and refine ui components (#830)

This commit is contained in:
Luke Vella 2023-08-29 17:29:26 +01:00 committed by GitHub
parent 1b100481a0
commit d261ef0d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 321 additions and 238 deletions

View file

@ -156,14 +156,11 @@
"goToInvite": "Go to Invite Page", "goToInvite": "Go to Invite Page",
"planPro": "Pro", "planPro": "Pro",
"Billing": "Billing", "Billing": "Billing",
"annualBilling": "Annual billing (Save {discount}%)",
"planUpgrade": "Upgrade", "planUpgrade": "Upgrade",
"subscriptionUpdatePayment": "Update Payment Details", "subscriptionUpdatePayment": "Update Payment Details",
"subscriptionCancel": "Cancel Subscription", "subscriptionCancel": "Cancel Subscription",
"billingStatus": "Billing Status", "billingStatus": "Billing Status",
"billingStatusDescription": "Manage your subscription and billing details", "billingStatusDescription": "Manage your subscription and billing details",
"subscriptionPlans": "Plans",
"subscriptionDescription": "Get access to more features by upgrading to a paid plan.",
"freeForever": "free forever", "freeForever": "free forever",
"annualBillingDescription": "per month, billed annually", "annualBillingDescription": "per month, billed annually",
"billingStatusState": "Status", "billingStatusState": "Status",
@ -176,12 +173,8 @@
"billingPeriod": "Period", "billingPeriod": "Period",
"billingPeriodMonthly": "Monthly", "billingPeriodMonthly": "Monthly",
"billingPeriodYearly": "Yearly", "billingPeriodYearly": "Yearly",
"plan_unlimitedPolls": "Unlimited polls",
"plan_unlimitedParticipants": "Unlimited participants",
"monthlyBillingDescription": "per month", "monthlyBillingDescription": "per month",
"plan_finalizePolls": "Finalize polls",
"plan_extendedPollLife": "Keep polls indefinitely", "plan_extendedPollLife": "Keep polls indefinitely",
"plan_prioritySupport": "Priority support",
"becomeATranslator": "Help translate", "becomeATranslator": "Help translate",
"noPolls": "No polls", "noPolls": "No polls",
"noPollsDescription": "Get started by creating a new poll.", "noPollsDescription": "Get started by creating a new poll.",
@ -200,7 +193,6 @@
"hideScoresDescription": "Only show scores until after a participant has voted", "hideScoresDescription": "Only show scores until after a participant has voted",
"disableComments": "Disable comments", "disableComments": "Disable comments",
"disableCommentsDescription": "Remove the option to leave a comment on the poll", "disableCommentsDescription": "Remove the option to leave a comment on the poll",
"planCustomizablePollSettings": "Customizable poll settings",
"clockPreferences": "Clock Preferences", "clockPreferences": "Clock Preferences",
"clockPreferencesDescription": "Set your preferred time zone and time format.", "clockPreferencesDescription": "Set your preferred time zone and time format.",
"featureRequest": "Request a Feature", "featureRequest": "Request a Feature",
@ -230,5 +222,15 @@
"billingPortal": "Billing Portal", "billingPortal": "Billing Portal",
"supportDescription": "Need help with anything?", "supportDescription": "Need help with anything?",
"supportBilling": "Please reach out if you need any assistance.", "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 <b>{date}</b>.",
"upgradeNowSaveLater": "Upgrade now, save later"
} }

View file

@ -1,159 +1,184 @@
import { import { CheckCircle2Icon, TrendingUpIcon } from "@rallly/icons";
BillingPlan, import { cn } from "@rallly/ui";
BillingPlanFooter, import { BillingPlanPeriod, BillingPlanPrice } from "@rallly/ui/billing-plan";
BillingPlanHeader, import { Button } from "@rallly/ui/button";
BillingPlanPeriod, import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
BillingPlanPerk, import dayjs from "dayjs";
BillingPlanPerks,
BillingPlanPrice,
BillingPlanTitle,
} from "@rallly/ui/billing-plan";
import { Label } from "@rallly/ui/label";
import { Switch } from "@rallly/ui/switch";
import React from "react"; import React from "react";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { UpgradeButton } from "@/components/upgrade-button"; import { UpgradeButton } from "@/components/upgrade-button";
import { usePlan } from "@/contexts/plan";
const monthlyPriceUsd = 5; const monthlyPriceUsd = 5;
const annualPriceUsd = 30; const annualPriceUsd = 30;
const Perks = ({ children }: React.PropsWithChildren) => {
return <ul className="grid gap-1">{children}</ul>;
};
const Perk = ({
children,
pro,
}: React.PropsWithChildren<{ pro?: boolean }>) => {
return (
<li className="flex items-start gap-x-2.5">
<CheckCircle2Icon
className={cn(
"mt-0.5 h-4 w-4 shrink-0",
!pro ? "text-gray-500" : "text-primary",
)}
/>
<div className="text-sm">{children}</div>
</li>
);
};
export const BillingPlans = () => { export const BillingPlans = () => {
const [isBilledAnnually, setBilledAnnually] = React.useState(true); const [tab, setTab] = React.useState("yearly");
const plan = usePlan();
const isPlus = plan === "paid";
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <Tabs value={tab} onValueChange={setTab}>
<Label className="mb-4"> <TabsList className="mb-4">
<Trans i18nKey="subscriptionPlans" defaults="Plans" /> <TabsTrigger value="monthly">
</Label> <Trans i18nKey="billingPeriodMonthly" />
<p className="text-muted-foreground mb-4 text-sm"> </TabsTrigger>
<TabsTrigger value="yearly">
<Trans i18nKey="billingPeriodYearly" />
</TabsTrigger>
</TabsList>
<div className="grid gap-4 rounded-md md:grid-cols-2">
<div className="space-y-4 rounded-md border p-4">
<div>
<h3>
<Trans i18nKey="planFree" />
</h3>
<p className="text-muted-foreground text-sm">
<Trans
i18nKey="planFreeDescription"
defaults="For casual users"
/>
</p>
</div>
<div>
<BillingPlanPrice>$0</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="freeForever" />
</BillingPlanPeriod>
</div>
<hr />
<Button disabled className="w-full">
<Trans i18nKey="currentPlan" defaults="Current Plan" />
</Button>
<Perks>
<Perk>
<Trans
i18nKey="limitedAccess"
defaults="Access to core features"
/>
</Perk>
<Perk>
<Trans
i18nKey="pollsDeleted"
defaults="Polls are automatically deleted once they become inactive"
/>
</Perk>
</Perks>
</div>
<div className="space-y-4 rounded-md border p-4">
<div>
<h3>
<Trans i18nKey="planPro" />
</h3>
<p className="text-muted-foreground text-sm">
<Trans
i18nKey="planProDescription"
defaults="For power users and professionals"
/>
</p>
</div>
<div className="flex">
<TabsContent value="yearly">
<BillingPlanPrice
discount={`$${(annualPriceUsd / 12).toFixed(2)}`}
>
${monthlyPriceUsd}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="annualBillingDescription"
defaults="per month, billed annually"
/>
</BillingPlanPeriod>
</TabsContent>
<TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="monthlyBillingDescription"
defaults="per month"
/>
</BillingPlanPeriod>
</TabsContent>
</div>
<hr />
<UpgradeButton annual={tab === "yearly"} />
<Perks>
<Perk pro={true}>
<Trans
i18nKey="accessAllFeatures"
defaults="Access all features"
/>
</Perk>
<Perk pro={true}>
<Trans i18nKey="plan_extendedPollLife" />
</Perk>
<Perk pro={true}>
<Trans
i18nKey="earlyAccess"
defaults="Get early access to new features"
/>
</Perk>
</Perks>
</div>
</div>
</Tabs>
<div className="rounded-md border border-cyan-200 bg-cyan-50 px-4 py-3 text-cyan-800">
<div className="flex items-start justify-between">
<TrendingUpIcon className="text-indigo mb-4 mr-2 mt-0.5 h-6 w-6 shrink-0 text-cyan-500" />
</div>
<div className="mb-2 flex items-center gap-x-2">
<h3 className="text-sm">
<Trans
i18nKey="upgradeNowSaveLater"
defaults="Upgrade now, save later"
/>
</h3>
</div>
<p className="-200 mb-4 text-sm">
<Trans <Trans
i18nKey="subscriptionDescription" i18nKey="earlyAdopterDescription"
defaults="By subscribing, you not only gain access to all features but you are also directly supporting further development of Rallly." defaults="As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases."
/> />
</p> </p>
</div> <p className="text-sm">
<div className="flex items-center gap-2.5">
<Switch
id="annual-switch"
checked={isBilledAnnually}
onCheckedChange={(checked) => {
setBilledAnnually(checked);
}}
/>
<Label htmlFor="annual-switch">
<Trans <Trans
i18nKey="annualBilling" i18nKey="nextPriceIncrease"
defaults="Annual billing (Save {discount}%)" defaults="Next price increase is scheduled for <b>{date}</b>."
components={{
b: (
<a
href="https://rallly.co/blog/july-recap"
className="font-bold hover:underline"
/>
),
}}
values={{ values={{
discount: Math.round(100 - (annualPriceUsd / 12 / 5) * 100), date: dayjs("2023-09-01").format("LL"),
}} }}
/> />
</Label> </p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<BillingPlan>
<BillingPlanHeader>
<BillingPlanTitle>
<Trans i18nKey="planFree" defaults="Free" />
</BillingPlanTitle>
<BillingPlanPrice>$0</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="freeForever" defaults="free forever" />
</BillingPlanPeriod>
</BillingPlanHeader>
<BillingPlanPerks>
<BillingPlanPerk>
<Trans i18nKey="plan_unlimitedPolls" defaults="Unlimited polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_unlimitedParticipants"
defaults="Unlimited participants"
/>
</BillingPlanPerk>
</BillingPlanPerks>
</BillingPlan>
<ProPlan annual={isBilledAnnually}>
{!isPlus ? (
<UpgradeButton annual={isBilledAnnually}>
<Trans i18nKey="planUpgrade" defaults="Upgrade" />
</UpgradeButton>
) : null}
</ProPlan>
</div> </div>
</div> </div>
); );
}; };
export const ProPlan = ({
annual,
children,
}: React.PropsWithChildren<{
annual?: boolean;
}>) => {
return (
<BillingPlan variant="primary">
<BillingPlanHeader>
<BillingPlanTitle className="text-primary">
<Trans i18nKey="planPro" defaults="Pro" />
</BillingPlanTitle>
{annual ? (
<>
<BillingPlanPrice discount={`$${(annualPriceUsd / 12).toFixed(2)}`}>
${monthlyPriceUsd}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="annualBillingDescription"
defaults="per month, billed annually"
/>
</BillingPlanPeriod>
</>
) : (
<>
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="monthlyBillingDescription" defaults="per month" />
</BillingPlanPeriod>
</>
)}
</BillingPlanHeader>
<BillingPlanPerks>
<BillingPlanPerk>
<Trans i18nKey="plan_unlimitedPolls" defaults="Unlimited polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_unlimitedParticipants"
defaults="Unlimited participants"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans i18nKey="plan_finalizePolls" defaults="Finalize polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="planCustomizablePollSettings"
defaults="Customizable poll settings"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_extendedPollLife"
defaults="Extended poll life"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans i18nKey="plan_prioritySupport" defaults="Priority support" />
</BillingPlanPerk>
</BillingPlanPerks>
<BillingPlanFooter>{children}</BillingPlanFooter>
</BillingPlan>
);
};

View file

@ -2,9 +2,11 @@ import { trpc } from "@rallly/backend";
import { HelpCircleIcon } from "@rallly/icons"; import { HelpCircleIcon } from "@rallly/icons";
import { cn } from "@rallly/ui"; import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import Script from "next/script"; import Script from "next/script";
import React from "react"; import React from "react";
import Tooltip from "@/components/tooltip";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
@ -35,24 +37,28 @@ export const Changelog = ({ className }: { className?: string }) => {
return ( return (
<> <>
<FeaturebaseScript /> <FeaturebaseScript />
<Button <Tooltip>
className={cn( <TooltipTrigger>
"hidden sm:inline-flex [&>*]:pointer-events-none", <Button
className, className={cn(
)} "hidden sm:inline-flex [&>*]:pointer-events-none",
size="sm" className,
variant="ghost" )}
data-featurebase-changelog size="sm"
> variant="ghost"
<HelpCircleIcon className="h-4 w-4" /> data-featurebase-changelog
<span className="hidden sm:block"> >
<HelpCircleIcon className="h-4 w-4" />
<span
id="fb-update-badge"
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
/>
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans key="whatsNew" defaults="What's new?" /> <Trans key="whatsNew" defaults="What's new?" />
</span> </TooltipContent>
<span </Tooltip>
id="fb-update-badge"
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
/>
</Button>
</> </>
); );
}; };

View file

@ -1,4 +1,4 @@
import { ListIcon, LogInIcon } from "@rallly/icons"; import { ListIcon, LogInIcon, SparklesIcon } from "@rallly/icons";
import { cn } from "@rallly/ui"; import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import clsx from "clsx"; import clsx from "clsx";
@ -16,6 +16,7 @@ import FeedbackButton from "@/components/feedback";
import { Spinner } from "@/components/spinner"; import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { UserDropdown } from "@/components/user-dropdown"; import { UserDropdown } from "@/components/user-dropdown";
import { IfFreeUser } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
import { DayjsProvider } from "@/utils/dayjs"; import { DayjsProvider } from "@/utils/dayjs";
@ -56,6 +57,17 @@ const NavMenuItem = ({
); );
}; };
const Upgrade = () => {
return (
<Button variant="primary" size="sm" asChild>
<Link href="/settings/billing">
<SparklesIcon className="-ml-0.5 h-4 w-4" />
<Trans i18nKey="upgrade" defaults="Upgrade" />
</Link>
</Button>
);
};
const Logo = () => { const Logo = () => {
const router = useRouter(); const router = useRouter();
const [isBusy, setIsBusy] = React.useState(false); const [isBusy, setIsBusy] = React.useState(false);
@ -106,7 +118,7 @@ const MainNav = () => {
}} }}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
exit={"hidden"} exit="hidden"
className="border-b bg-gray-50/50" className="border-b bg-gray-50/50"
> >
<Container className="flex h-14 items-center justify-between gap-4"> <Container className="flex h-14 items-center justify-between gap-4">
@ -121,7 +133,10 @@ const MainNav = () => {
</nav> </nav>
</div> </div>
<div className="flex items-center gap-x-4"> <div className="flex items-center gap-x-4">
<nav className="flex items-center gap-x-1 sm:gap-x-2"> <nav className="flex items-center gap-x-1 sm:gap-x-1.5">
<IfFreeUser>
<Upgrade />
</IfFreeUser>
<IfGuest> <IfGuest>
<Button <Button
size="sm" size="sm"

View file

@ -7,9 +7,7 @@ export const SettingsSection = (props: {
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-4 p-3 sm:p-6 md:grid-cols-3"> <div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-4 p-3 sm:p-6 md:grid-cols-3">
<div> <div>
<h2 className="text-base font-semibold">{props.title}</h2> <h2 className="text-base font-semibold">{props.title}</h2>
<p className="mt-1 text-sm leading-6 text-gray-500"> <p className="mt-1 text-sm text-gray-500">{props.description}</p>
{props.description}
</p>
</div> </div>
<div className="md:col-span-2">{props.children}</div> <div className="md:col-span-2">{props.children}</div>
</div> </div>

View file

@ -1,5 +1,6 @@
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import Link from "next/link"; import Link from "next/link";
import { Trans } from "next-i18next";
import React from "react"; import React from "react";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
@ -11,23 +12,21 @@ export const UpgradeButton = ({
const posthog = usePostHog(); const posthog = usePostHog();
return ( return (
<> <Button
<Button className="w-full"
className="w-full" variant="primary"
variant="primary" asChild
asChild onClick={() => {
onClick={() => { posthog?.capture("click upgrade button");
posthog?.capture("click upgrade button"); }}
}} >
<Link
href={`/api/stripe/checkout?period=${
annual ? "yearly" : "monthly"
}&return_path=${encodeURIComponent(window.location.pathname)}`}
> >
<Link {children || <Trans i18nKey="upgrade" defaults="Upgrade" />}
href={`/api/stripe/checkout?period=${ </Link>
annual ? "yearly" : "monthly" </Button>
}&return_path=${encodeURIComponent(window.location.pathname)}`}
>
{children}
</Link>
</Button>
</>
); );
}; };

View file

@ -11,7 +11,6 @@ import {
UserIcon, UserIcon,
UserPlusIcon, UserPlusIcon,
} from "@rallly/icons"; } from "@rallly/icons";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -25,27 +24,11 @@ import Link from "next/link";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { CurrentUserAvatar } from "@/components/user"; import { CurrentUserAvatar } from "@/components/user";
import { usePlan } from "@/contexts/plan"; import { Plan } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
import { IfAuthenticated, IfGuest, useUser } from "./user-provider"; import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
const Plan = () => {
const plan = usePlan();
if (plan === "paid") {
return (
<Badge>
<Trans i18nKey="planPro" defaults="Pro" />
</Badge>
);
}
return (
<Badge variant="secondary">
<Trans i18nKey="planFree" defaults="Free" />
</Badge>
);
};
export const UserDropdown = () => { export const UserDropdown = () => {
const { user } = useUser(); const { user } = useUser();
return ( return (

View file

@ -1,4 +1,8 @@
import { trpc } from "@rallly/backend"; import { trpc } from "@rallly/backend";
import { Badge } from "@rallly/ui/badge";
import React from "react";
import { Trans } from "@/components/trans";
export const usePlan = () => { export const usePlan = () => {
const { data } = trpc.user.subscription.useQuery(); const { data } = trpc.user.subscription.useQuery();
@ -7,3 +11,32 @@ export const usePlan = () => {
return isPaid ? "paid" : "free"; 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 (
<Badge>
<Trans i18nKey="planPro" defaults="Pro" />
</Badge>
);
}
return (
<Badge variant="secondary">
<Trans i18nKey="planFree" defaults="Free" />
</Badge>
);
};

View file

@ -1,6 +1,5 @@
import { trpc } from "@rallly/backend"; import { trpc } from "@rallly/backend";
import { ArrowUpRight, CreditCardIcon } from "@rallly/icons"; import { ArrowUpRight, CreditCardIcon } from "@rallly/icons";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { Card } from "@rallly/ui/card"; import { Card } from "@rallly/ui/card";
import { Label } from "@rallly/ui/label"; 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 { SettingsSection } from "@/components/settings/settings-section";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider"; import { useUser } from "@/components/user-provider";
import { Plan } from "@/contexts/plan";
import { NextPageWithLayout } from "../../types"; import { NextPageWithLayout } from "../../types";
import { getStaticTranslations } from "../../utils/with-page-translations"; import { getStaticTranslations } from "../../utils/with-page-translations";
@ -28,12 +28,10 @@ declare global {
const BillingPortal = () => { const BillingPortal = () => {
return ( return (
<div> <div>
<div className="mb-4 flex items-center justify-between"> <Label className="mb-2">
<Badge> <Trans i18nKey="billingPortal" />
<Trans i18nKey="planPro" /> </Label>
</Badge> <p className="text-sm">
</div>
<p>
<Trans <Trans
i18nKey="activeSubscription" i18nKey="activeSubscription"
defaults="Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal." defaults="Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal."
@ -76,14 +74,30 @@ const SubscriptionStatus = () => {
return <>You need to be logged in.</>; return <>You need to be logged in.</>;
} }
if (data.legacy) { return (
// User is on the old billing system <div className="space-y-6">
return <LegacyBilling />; <div>
} else if (data.active) { <Label className="mb-2">
return <BillingPortal />; <Trans i18nKey="currentPlan" />
} else { </Label>
return <BillingPlans />; <div>
} <Plan />
</div>
</div>
{!data.active ? (
<div>
<Label className="mb-4">
<Trans i18nKey="upgrade" />
</Label>
<BillingPlans />
</div>
) : data.legacy ? (
<LegacyBilling />
) : (
<BillingPortal />
)}
</div>
);
}; };
const LegacyBilling = () => { const LegacyBilling = () => {
@ -229,7 +243,7 @@ const Page: NextPageWithLayout = () => {
} }
> >
<div className="space-y-6"> <div className="space-y-6">
<p> <p className="text-sm">
<Trans <Trans
i18nKey="supportBilling" i18nKey="supportBilling"
defaults="Please reach out if you need any assistance." defaults="Please reach out if you need any assistance."

View file

@ -20,16 +20,18 @@ export const user = router({
}, },
}); });
}), }),
subscription: possiblyPublicProcedure.query(async ({ ctx }) => { subscription: possiblyPublicProcedure.query(
if (ctx.user.isGuest) { async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
// guest user can't have an active subscription if (ctx.user.isGuest) {
return { // guest user can't have an active subscription
active: false, return {
}; active: false,
} };
}
return await getSubscriptionStatus(ctx.user.id); return await getSubscriptionStatus(ctx.user.id);
}), },
),
changeName: privateProcedure changeName: privateProcedure
.input( .input(
z.object({ z.object({

View file

@ -9,6 +9,7 @@ export const getSubscriptionStatus = async (userId: string) => {
subscription: { subscription: {
select: { select: {
active: true, active: true,
periodEnd: true,
}, },
}, },
}, },
@ -17,6 +18,7 @@ export const getSubscriptionStatus = async (userId: string) => {
if (user?.subscription?.active === true) { if (user?.subscription?.active === true) {
return { return {
active: true, active: true,
expiresAt: user.subscription.periodEnd,
}; };
} }
@ -27,6 +29,9 @@ export const getSubscriptionStatus = async (userId: string) => {
gt: new Date(), gt: new Date(),
}, },
}, },
select: {
endDate: true,
},
}); });
if ( if (
@ -36,6 +41,7 @@ export const getSubscriptionStatus = async (userId: string) => {
return { return {
active: true, active: true,
legacy: true, legacy: true,
expiresAt: userPaymentData.endDate,
}; };
} }

View file

@ -6,7 +6,7 @@ import * as React from "react";
import { cn } from "./lib/utils"; import { cn } from "./lib/utils";
const buttonVariants = cva( 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: { variants: {
variant: { variant: {
@ -22,8 +22,8 @@ const buttonVariants = cva(
link: "underline-offset-4 hover:underline text-primary", link: "underline-offset-4 hover:underline text-primary",
}, },
size: { size: {
default: "h-9 px-2.5 text-sm", default: "h-9 px-2.5 gap-x-2.5 text-sm",
sm: "h-7 text-xs px-1.5 rounded-md", sm: "h-7 text-xs px-2 gap-x-1.5 rounded-md",
lg: "h-11 text-base px-4 rounded-md", lg: "h-11 text-base px-4 rounded-md",
}, },
}, },
@ -78,7 +78,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{loading ? ( {loading ? (
<SpinnerIcon className="inline-block h-4 w-4 animate-spin" /> <SpinnerIcon className="inline-block h-4 w-4 animate-spin" />
) : Icon ? ( ) : Icon ? (
<Icon className="h-4 w-4" /> <Icon className="-ml-0.5 h-4 w-4" />
) : null} ) : null}
{children} {children}
</> </>

View file

@ -51,7 +51,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 shadow-huge fixed z-50 grid w-full gap-4 overflow-hidden bg-white p-5 sm:rounded-md", "sm:zoom-in-90 shadow-huge fixed z-50 grid w-full gap-4 overflow-hidden bg-white p-5 sm:rounded-md",
{ {
"sm:max-w-sm": size === "sm", "sm:max-w-sm": size === "sm",
"sm:max-w-md": size === "md", "sm:max-w-md": size === "md",
@ -62,14 +62,14 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"> {!hideCloseButton ? (
{!hideCloseButton ? ( <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<> <>
<XIcon className="h-4 w-4" /> <XIcon className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</> </>
) : null} </DialogPrimitive.Close>
</DialogPrimitive.Close> ) : null}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)); ));

View file

@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", "ring-offset-background focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className, className,
)} )}
{...props} {...props}