shadcn pricing section: plans from Stripe
app/pricing/page.tsx
import Stripe from "stripe";
import type { Plan } from "@/components/pricing-switch/PricingSwitch";
import { PricingSection } from "./pricing-section";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// One Stripe Product per plan, with two recurring Prices found by lookup key:
// "<plan>_monthly" and "<plan>_yearly". The card copy lives on the Product.
const PLAN_IDS = ["starter", "pro", "team"];
export const revalidate = 3600; // re-read Stripe at most once an hour
async function getPlans(): Promise<Plan[]> {
const { data } = await stripe.prices.list({
lookup_keys: PLAN_IDS.flatMap((id) => [`${id}_monthly`, `${id}_yearly`]),
active: true,
expand: ["data.product"],
});
const byKey = new Map(data.map((p) => [p.lookup_key, p]));
return PLAN_IDS.flatMap((id): Plan[] => {
const m = byKey.get(`${id}_monthly`);
const y = byKey.get(`${id}_yearly`);
if (!m?.unit_amount || !y?.unit_amount || typeof m.product !== "object" || m.product.deleted) return [];
const product = m.product;
return [{
id,
name: product.name,
description: product.description ?? "",
note: product.metadata.note || undefined,
// Pricing Switch shows whole numbers per month; amounts from Stripe are in cents
monthly: Math.round(m.unit_amount / 100),
yearly: Math.round(y.unit_amount / 100 / 12),
cta: product.metadata.cta || `Choose ${product.name}`,
features: product.marketing_features.flatMap((f) => (f.name ? [f.name] : [])),
featured: product.metadata.featured === "true",
}];
});
}
export default async function PricingPage() {
const plans = await getPlans();
return <PricingSection plans={plans} />;
}
app/pricing/pricing-section.tsx
"use client";
import { PricingSwitch, type Plan } from "@/components/pricing-switch/PricingSwitch";
export function PricingSection({ plans }: { plans: Plan[] }) {
// send only the plan id and period; the server looks the price up again
const choose = async (plan: Plan, yearly: boolean) => {
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ plan: plan.id, yearly }),
});
const { url } = (await res.json()) as { url?: string | null };
if (res.ok && url) window.location.assign(url);
};
return (
<PricingSwitch
plans={plans}
onSelect={choose}
yearlyBadge="2 months free"
header={
<div>
<h2 className="text-3xl font-semibold tracking-tight">Pricing</h2>
<p className="text-muted-foreground">Switch to yearly and the prices roll to the new numbers.</p>
</div>
}
/>
);
}