Skip to content

Commit a850078

Browse files
authored
docs: add yearly pricing (#2597)
1 parent cd92dc2 commit a850078

File tree

5 files changed

+120
-15
lines changed

5 files changed

+120
-15
lines changed

docs/app/pricing/PricingTiers.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import { cn } from "@/lib/fumadocs/cn";
4+
import { useState } from "react";
5+
import { Tier, Tiers } from "./tiers";
6+
7+
type Frequency = "month" | "year";
8+
9+
export function PricingTiers({ tiers }: { tiers: Tier[] }) {
10+
const [frequency, setFrequency] = useState<Frequency>("year");
11+
12+
return (
13+
<>
14+
{/* Frequency Toggle */}
15+
<div className="mb-10 flex items-center justify-center gap-3">
16+
<span
17+
className={cn(
18+
"text-sm font-medium transition-colors",
19+
frequency === "month" ? "text-stone-900" : "text-stone-400",
20+
)}
21+
>
22+
Monthly
23+
</span>
24+
<button
25+
type="button"
26+
role="switch"
27+
aria-checked={frequency === "year"}
28+
onClick={() =>
29+
setFrequency((f) => (f === "month" ? "year" : "month"))
30+
}
31+
className={cn(
32+
"relative inline-flex h-7 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
33+
frequency === "year" ? "bg-purple-600" : "bg-stone-300",
34+
)}
35+
>
36+
<span
37+
className={cn(
38+
"pointer-events-none inline-block h-6 w-6 rounded-full bg-white shadow-sm ring-0 transition-transform",
39+
frequency === "year" ? "translate-x-5" : "translate-x-0",
40+
)}
41+
/>
42+
</button>
43+
<span
44+
className={cn(
45+
"text-sm font-medium transition-colors",
46+
frequency === "year" ? "text-stone-900" : "text-stone-400",
47+
)}
48+
>
49+
Yearly
50+
</span>
51+
<span className="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-700">
52+
Save 50%
53+
</span>
54+
</div>
55+
56+
<Tiers tiers={tiers} frequency={frequency} />
57+
</>
58+
);
59+
}

docs/app/pricing/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FAQ } from "@/app/pricing/faq";
2-
import { Tier, Tiers } from "@/app/pricing/tiers";
2+
import { Tier } from "@/app/pricing/tiers";
33
import { InfiniteSlider } from "@/components/InfiniteSlider";
44
import {
55
Tooltip,
@@ -9,6 +9,7 @@ import {
99
} from "@/components/ui/tooltip";
1010
import { getFullMetadata } from "@/lib/getFullMetadata";
1111
import Link from "next/link";
12+
import { PricingTiers } from "./PricingTiers";
1213

1314
export const metadata = getFullMetadata({
1415
title: "Pricing",
@@ -80,7 +81,7 @@ const tiers: Tier[] = [
8081
badge: "Recommended",
8182
description:
8283
"Commercial license for access to advanced features and technical support.",
83-
price: { month: 390, year: 48 },
84+
price: { month: 390, year: 2340 },
8485
features: [
8586
<span key="commercial" className="font-semibold text-stone-900">
8687
Commercial license for XL packages:
@@ -162,8 +163,8 @@ export default function Pricing() {
162163
</p>
163164
</div>
164165

165-
{/* Pricing Tiers */}
166-
<Tiers tiers={tiers} frequency="month" />
166+
{/* Pricing Tiers with Toggle */}
167+
<PricingTiers tiers={tiers} />
167168

168169
{/* Social proof */}
169170
<div className="mt-24 w-full border-t border-stone-200 pt-16">

docs/app/pricing/tiers.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ export type Tier = {
2323
cta?: "get-started" | "buy" | "contact";
2424
};
2525

26-
function TierCTAButton({ tier }: { tier: Tier }) {
26+
const BUSINESS_PLAN_TYPES = new Set(["business", "business-yearly"]);
27+
28+
function isBusinessPlan(planType: string) {
29+
return BUSINESS_PLAN_TYPES.has(planType);
30+
}
31+
32+
function TierCTAButton({ tier, frequency }: { tier: Tier; frequency: Frequency }) {
2733
const { data: session } = useSession();
2834
let text =
2935
tier.cta === "get-started"
@@ -38,10 +44,11 @@ function TierCTAButton({ tier }: { tier: Tier }) {
3844
if (session.planType === "free") {
3945
text = "Buy now";
4046
} else {
41-
text =
42-
session.planType === tier.id
43-
? "Manage subscription"
44-
: "Update subscription";
47+
const isCurrentPlan =
48+
tier.id === "business"
49+
? isBusinessPlan(session.planType ?? "")
50+
: session.planType === tier.id;
51+
text = isCurrentPlan ? "Manage subscription" : "Update subscription";
4552
}
4653
}
4754

@@ -68,9 +75,6 @@ function TierCTAButton({ tier }: { tier: Tier }) {
6875
}
6976

7077
track("Signup", { tier: tier.id });
71-
// ... rest of analytic logic kept simple for brevity in replacement,
72-
// in real implementation we keep the existing logic.
73-
// Re-injecting existing analytics logic below to ensure no regression.
7478
if (!session) {
7579
Sentry.captureEvent({
7680
message: "click-pricing-signup",
@@ -90,9 +94,16 @@ function TierCTAButton({ tier }: { tier: Tier }) {
9094
track("click-pricing-buy-now", { tier: tier.id });
9195
e.preventDefault();
9296
e.stopPropagation();
93-
await authClient.checkout({ slug: tier.id });
97+
const checkoutSlug = frequency === "year" && tier.id === "business"
98+
? "business-yearly"
99+
: tier.id;
100+
await authClient.checkout({ slug: checkoutSlug });
94101
} else {
95-
if (session.planType === tier.id) {
102+
const isCurrentPlan =
103+
tier.id === "business"
104+
? isBusinessPlan(session.planType ?? "")
105+
: session.planType === tier.id;
106+
if (isCurrentPlan) {
96107
Sentry.captureEvent({
97108
message: "click-pricing-manage-subscription",
98109
level: "info",
@@ -208,6 +219,28 @@ export function Tiers({
208219
<span className="text-3xl font-bold text-stone-900">
209220
{tier.price}
210221
</span>
222+
) : frequency === "year" ? (
223+
<div>
224+
<div className="flex items-baseline gap-2">
225+
<span className="text-4xl font-bold text-stone-900">
226+
${Math.round(tier.price.year / 12)}
227+
</span>
228+
<span className="text-sm font-medium text-stone-400">
229+
/month
230+
</span>
231+
</div>
232+
<div className="mt-1.5 flex items-center gap-2">
233+
<span className="text-sm text-stone-400 line-through decoration-stone-400">
234+
${tier.price.month}/mo
235+
</span>
236+
<span className="rounded-md bg-green-100 px-1.5 py-0.5 text-xs font-semibold text-green-700">
237+
now -50%
238+
</span>
239+
</div>
240+
<p className="mt-1 text-xs text-stone-400">
241+
${tier.price.year.toLocaleString()} billed yearly
242+
</p>
243+
</div>
211244
) : (
212245
<div className="flex items-baseline gap-1">
213246
<span className="text-4xl font-bold text-stone-900">
@@ -227,7 +260,7 @@ export function Tiers({
227260

228261
{/* CTA */}
229262
<div className="mb-6">
230-
<TierCTAButton tier={tier} />
263+
<TierCTAButton tier={tier} frequency={frequency} />
231264
</div>
232265

233266
{/* Features */}

docs/lib/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ export const auth = betterAuth({
207207
productId: PRODUCTS.business.id, // ID of Product from Polar Dashboard
208208
slug: PRODUCTS.business.slug, // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
209209
},
210+
{
211+
productId: PRODUCTS["business-yearly"].id,
212+
slug: PRODUCTS["business-yearly"].slug,
213+
},
210214
{
211215
productId: PRODUCTS.starter.id,
212216
slug: PRODUCTS.starter.slug,

docs/lib/product-list.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ export const PRODUCTS = {
77
name: "Business",
88
slug: "business",
99
} as const,
10+
"business-yearly": {
11+
id:
12+
process.env.NODE_ENV === "production"
13+
? "ba3965dc-e1ca-494e-b36a-62e2e41615d4"
14+
: "NOT-CREATED",
15+
name: "Business Yearly",
16+
slug: "business-yearly",
17+
} as const,
1018
starter: {
1119
id:
1220
process.env.NODE_ENV === "production"

0 commit comments

Comments
 (0)