Pricing Section

Tiered pricing with billing toggle and Stripe checkout integration.

Features

  • • 3-tier pricing (Free, Pro, Ultra)
  • • Monthly/yearly billing toggle
  • • Automatic savings calculation
  • • Popular plan highlighting

Conversion

  • • Yearly billing highlighted
  • • Clear savings indicators
  • • Auth-aware buttons
  • • Responsive design

Plan Tiers

Free

$0 forever

Pro ⭐

$20/mo or $200/yr

Ultra

$50/mo or $500/yr

Plans defined in src/lib/subscriptions.ts

Pricing Component

src/components/PricingSection.tsx
1// src/components/PricingSection.tsx
2import { auth } from "~/auth";
3import { db } from "~/server/db";
4
5export async function PricingSection() {
6 const session = await auth();
7
8 const subscription = session?.user?.id
9 ? await db.subscription.findUnique({
10 where: { userId: session.user.id },
11 })
12 : null;
13
14 return (
15 <section className="py-24">
16 <div className="text-center mb-16">
17 <h2 className="text-4xl font-bold">Simple Pricing</h2>
18 </div>
19 <div className="grid md:grid-cols-3 gap-8">
20 {plans.map((plan) => (
21 <PricingCard
22 key={plan.id}
23 plan={plan}
24 isLoggedIn={!!session?.user}
25 />
26 ))}
27 </div>
28 </section>
29 );
30}

Pricing Card

1"use client";
2
3import { useState } from "react";
4import { createCheckoutSession } from "~/server/actions/stripe";
5
6export function PricingCard({ plan, isLoggedIn }) {
7 const [isLoading, setIsLoading] = useState(false);
8
9 const handleSubscribe = async () => {
10 if (!isLoggedIn) {
11 window.location.href = "/auth/signin";
12 return;
13 }
14
15 if (!plan.priceId) {
16 window.location.href = "/account";
17 return;
18 }
19
20 setIsLoading(true);
21 const result = await createCheckoutSession(plan.priceId);
22 if (result.url) window.location.href = result.url;
23 setIsLoading(false);
24 };
25
26 return (
27 <div className={plan.popular ? "border-primary scale-105" : ""}>
28 {plan.popular && <div>Most Popular</div>}
29 <h3>{plan.name}</h3>
30 <p className="text-4xl font-bold">{plan.price}</p>
31 <ul>
32 {plan.features.map((f, i) => <li key={i}>{f}</li>)}
33 </ul>
34 <button onClick={handleSubscribe} disabled={isLoading}>
35 {plan.current ? "Current Plan" : plan.buttonText}
36 </button>
37 </div>
38 );
39}

Testing

Stripe Test Cards

  • 4242 4242 4242 4242 — Successful
  • 4000 0000 0000 0002 — Declined

Best Practices

Conversion

  • • Highlight most popular plan
  • • Show annual savings
  • • Include social proof
  • • Clear CTAs

Technical

  • • Handle loading states
  • • Validate server-side
  • • Test checkout flow
  • • Monitor conversions

Next: Email Templates

Transactional email designs

Continue →