mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-13 17:06: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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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>
|
||||||
));
|
));
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue