mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-13 00:46:48 +02:00
💄 Update billing page and refine ui components (#830)
This commit is contained in:
parent
1b100481a0
commit
d261ef0d59
14 changed files with 321 additions and 238 deletions
|
@ -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 <b>{date}</b>.",
|
||||
"upgradeNowSaveLater": "Upgrade now, save later"
|
||||
}
|
||||
|
|
|
@ -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 <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 = () => {
|
||||
const [isBilledAnnually, setBilledAnnually] = React.useState(true);
|
||||
const plan = usePlan();
|
||||
const isPlus = plan === "paid";
|
||||
const [tab, setTab] = React.useState("yearly");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="mb-4">
|
||||
<Trans i18nKey="subscriptionPlans" defaults="Plans" />
|
||||
</Label>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="monthly">
|
||||
<Trans i18nKey="billingPeriodMonthly" />
|
||||
</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
|
||||
i18nKey="subscriptionDescription"
|
||||
defaults="By subscribing, you not only gain access to all features but you are also directly supporting further development of Rallly."
|
||||
i18nKey="earlyAdopterDescription"
|
||||
defaults="As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Switch
|
||||
id="annual-switch"
|
||||
checked={isBilledAnnually}
|
||||
onCheckedChange={(checked) => {
|
||||
setBilledAnnually(checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="annual-switch">
|
||||
<p className="text-sm">
|
||||
<Trans
|
||||
i18nKey="annualBilling"
|
||||
defaults="Annual billing (Save {discount}%)"
|
||||
i18nKey="nextPriceIncrease"
|
||||
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={{
|
||||
discount: Math.round(100 - (annualPriceUsd / 12 / 5) * 100),
|
||||
date: dayjs("2023-09-01").format("LL"),
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</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>
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<FeaturebaseScript />
|
||||
<Button
|
||||
className={cn(
|
||||
"hidden sm:inline-flex [&>*]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
data-featurebase-changelog
|
||||
>
|
||||
<HelpCircleIcon className="h-4 w-4" />
|
||||
<span className="hidden sm:block">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className={cn(
|
||||
"hidden sm:inline-flex [&>*]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
data-featurebase-changelog
|
||||
>
|
||||
<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?" />
|
||||
</span>
|
||||
<span
|
||||
id="fb-update-badge"
|
||||
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<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 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"
|
||||
>
|
||||
<Container className="flex h-14 items-center justify-between gap-4">
|
||||
|
@ -121,7 +133,10 @@ const MainNav = () => {
|
|||
</nav>
|
||||
</div>
|
||||
<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>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
@ -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>
|
||||
<h2 className="text-base font-semibold">{props.title}</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-gray-500">
|
||||
{props.description}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500">{props.description}</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">{props.children}</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="primary"
|
||||
asChild
|
||||
onClick={() => {
|
||||
posthog?.capture("click upgrade button");
|
||||
}}
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="primary"
|
||||
asChild
|
||||
onClick={() => {
|
||||
posthog?.capture("click upgrade button");
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/api/stripe/checkout?period=${
|
||||
annual ? "yearly" : "monthly"
|
||||
}&return_path=${encodeURIComponent(window.location.pathname)}`}
|
||||
>
|
||||
<Link
|
||||
href={`/api/stripe/checkout?period=${
|
||||
annual ? "yearly" : "monthly"
|
||||
}&return_path=${encodeURIComponent(window.location.pathname)}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
{children || <Trans i18nKey="upgrade" defaults="Upgrade" />}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<Badge>
|
||||
<Trans i18nKey="planPro" defaults="Pro" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<Trans i18nKey="planFree" defaults="Free" />
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
export const UserDropdown = () => {
|
||||
const { user } = useUser();
|
||||
return (
|
||||
|
|
|
@ -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 (
|
||||
<Badge>
|
||||
<Trans i18nKey="planPro" defaults="Pro" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<Trans i18nKey="planFree" defaults="Free" />
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<Badge>
|
||||
<Trans i18nKey="planPro" />
|
||||
</Badge>
|
||||
</div>
|
||||
<p>
|
||||
<Label className="mb-2">
|
||||
<Trans i18nKey="billingPortal" />
|
||||
</Label>
|
||||
<p className="text-sm">
|
||||
<Trans
|
||||
i18nKey="activeSubscription"
|
||||
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.</>;
|
||||
}
|
||||
|
||||
if (data.legacy) {
|
||||
// User is on the old billing system
|
||||
return <LegacyBilling />;
|
||||
} else if (data.active) {
|
||||
return <BillingPortal />;
|
||||
} else {
|
||||
return <BillingPlans />;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="mb-2">
|
||||
<Trans i18nKey="currentPlan" />
|
||||
</Label>
|
||||
<div>
|
||||
<Plan />
|
||||
</div>
|
||||
</div>
|
||||
{!data.active ? (
|
||||
<div>
|
||||
<Label className="mb-4">
|
||||
<Trans i18nKey="upgrade" />
|
||||
</Label>
|
||||
<BillingPlans />
|
||||
</div>
|
||||
) : data.legacy ? (
|
||||
<LegacyBilling />
|
||||
) : (
|
||||
<BillingPortal />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyBilling = () => {
|
||||
|
@ -229,7 +243,7 @@ const Page: NextPageWithLayout = () => {
|
|||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p>
|
||||
<p className="text-sm">
|
||||
<Trans
|
||||
i18nKey="supportBilling"
|
||||
defaults="Please reach out if you need any assistance."
|
||||
|
|
|
@ -20,16 +20,18 @@ export const user = router({
|
|||
},
|
||||
});
|
||||
}),
|
||||
subscription: possiblyPublicProcedure.query(async ({ ctx }) => {
|
||||
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({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<HTMLButtonElement, ButtonProps>(
|
|||
{loading ? (
|
||||
<SpinnerIcon className="inline-block h-4 w-4 animate-spin" />
|
||||
) : Icon ? (
|
||||
<Icon className="h-4 w-4" />
|
||||
<Icon className="-ml-0.5 h-4 w-4" />
|
||||
) : null}
|
||||
{children}
|
||||
</>
|
||||
|
|
|
@ -51,7 +51,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
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-md": size === "md",
|
||||
|
@ -62,14 +62,14 @@ const DialogContent = React.forwardRef<
|
|||
{...props}
|
||||
>
|
||||
{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" />
|
||||
<span className="sr-only">Close</span>
|
||||
</>
|
||||
) : null}
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Close>
|
||||
) : null}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
|
|
|
@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
|
|||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue