Saas Subscription

Creating a product

Stripe product catalog

Stripe new product

Stripe created product

Stripe Product ID

Creating a price

Product price

Price API Key

env
1. . .
2
3STRIPE_SECRET_KEY="<stripe_secret_key>"
4STRIPE_PRICE_KEY_MONTH="<price_id>"

Creating the stripe checkout button

text
1src
2├── app
3├── components
4│   ├── email-template.jsx
5│   ├── footer.jsx
6│   ├── navbar.jsx
7│   ├── stripeButton.jsx     <===== Stripe button here
8│   └── ui
9│       └── button.jsx       <===== Button styles here
10└── libs

components/ui/button.jsx

jsx
1"use client";
2
3export default function Button({
4  style = "primary",
5  children,
6  loading = false,
7  className = "",
8  ...props
9}) {
10  const baseStyles = ". . .";
11
12  let styleClass = "";
13  if (style === "primary") {
14    styleClass = ". . .";
15  } else if (style === "danger") {
16    styleClass = ". . .";
17  }
18
19  const combinedClassName = `${baseStyles} ${styleClass} ${className}`;
20
21  return (
22    <button className={combinedClassName} disabled={loading} {...props}>
23      {children}
24      {loading && (
25        <svg
26          className="animate-spin h-5 w-5 text-white inline ml-2"
27          xmlns="http://www.w3.org/2000/svg"
28          fill="none"
29          viewBox="0 0 24 24">
30          . . .
31        </svg>
32      )}
33    </button>
34  );
35}

components/stripeButton.jsx

jsx
1"use client";
2
3import Button from "@/components/ui/button";
4import { useState } from "react";
5
6export default function StripeButton({ type, plan }) {
7  const [loading, setLoading] = useState(false);
8  const [message, setMessage] = useState("");
9  const [confirmOpen, setConfirmOpen] = useState(false);
10
11  async function checkout(plan) {
12    setLoading(true);
13
14    const response = await fetch(`/api/stripe/${plan}/checkout`, {
15      method: "POST",
16      headers: {
17        "Content-Type": "application/json",
18      },
19    });
20
21    setLoading(false);
22
23    if (response.ok) {
24      const data = await response.json();
25      window.open(data.url, "_target");
26    } else {
27      setMessage("Something went wrong.");
28    }
29  }
30
31  return (
32    <>
33      {type === "checkout" && (
34        <Button
35          style="primary"
36          loading={loading}
37          className="w-full text-center"
38          onClick={() => checkout(plan)}>
39          Get this plan
40        </Button>
41      )}
42
43      <p>{message}</p>
44    </>
45  );
46}

Handling checkout

text
1src/app/api
2├── auth
3└── stripe
4    ├── cancel
5    ├── monthly
6    │   └── checkout
7    │       └── route.js     <===== API handler for monthly subscription checkout
8    ├── onetime
9    │   └── checkout
10    ├── resume
11    └── webhook

api/stripe/subscribe/monthly/route.js

javascript
1import Stripe from "stripe";
2import { auth } from "@/libs/auth";
3
4export async function POST() {
5  // Initialize a new Stripe instance
6  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
7
8  // Get the user session
9  const session = await auth();
10
11  // If user is not signed in
12  if (!session) {
13    return Response.json(
14      {
15        message: "You are not signed in.",
16      },
17      { status: 401 }
18    );
19  }
20
21  // Create a Stripe checkout session
22  const checkoutSession = await stripe.checkout.sessions.create({
23    mode: "subscription",
24    customer: session?.user?.stripeCustomerId,
25    line_items: [
26      {
27        price: process.env.STRIPE_PRICE_KEY_MONTH,
28        quantity: 1,
29      },
30    ],
31
32    success_url: "http://localhost:3001/",
33    cancel_url: "http://localhost:3001/",
34  });
35
36  // If failed to create checkout session
37  if (!checkoutSession.url) {
38    return Response.json(
39      {
40        message: "Could not create checkout session.",
41      },
42      { status: 500 }
43    );
44  }
45
46  // If checkout session created successfully, send the checkout session url in response
47  return Response.json(
48    {
49      url: checkoutSession.url,
50      message: "Checkout session created successfully.",
51    },
52    {
53      status: 200,
54    }
55  );
56}

pricing/page.jsx

jsx
1import StripeButton from "@/components/stripeButton";
2
3export default function PricingPage() {
4  return (
5    <div className="bg-gray-50 py-12">
6      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
7        <div className="text-center">
8          <h2 className="text-3xl font-bold text-gray-900 sm:text-4xl">
9            Pricing Plans
10          </h2>
11        </div>
12
13        <div className="mt-16 space-y-8 lg:space-y-0 lg:grid lg:grid-cols-3 lg:gap-x-8">
14          <div className="rounded-lg bg-white p-8 text-center shadow-md">
15            <h3 className="text-2xl font-bold text-gray-900">Monthly Plan</h3>
16            <p className="mt-4 text-gray-600">
17              Full access to all course materials.
18            </p>
19            <div className="mt-6">
20              <span className="text-4xl font-bold text-gray-900">$29</span>
21              <span className="text-gray-600">/month</span>
22            </div>
23            <ul className="mt-6 space-y-4">. . .</ul>
24            <div className="mt-8">
25              <StripeButton plan={"monthly"} type={"checkout"} />
26            </div>
27          </div>
28          . . .
29        </div>
30      </div>
31    </div>
32  );
33}

Stripe Checkout

Setting up the webhook

text
1src/app/api
2├── auth
3└── stripe
4    ├── cancel
5    ├── monthly
6    │   └── checkout
7    │       └── route.js     <===== API handler for monthly subscription checkout
8    ├── onetime
9    │   └── checkout
10    ├── resume
11    └── webhook
12        └── route.js         <===== Create webhook here
bash
1stripe listen --forward-to localhost:3001/api/stripe/webhook
text
1> Ready! You are using Stripe API Version [2023-10-16]. Your webhook signing secret is whsec_xxx (^C to quit)

.env

env
1. . .
2
3STRIPE_SECRET_KEY="<stripe_secret_key>"
4STRIPE_PRICE_KEY_MONTH="<price_id>"
5STRIPE_WEBHOOK_SECRET="<webhook_secret>"

api/stripe/webhook/route.js

javascript
1import Stripe from "stripe";
2import prisma from "@/libs/db";
3import { headers } from "next/headers";
4
5export async function POST(request) {
6  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
7
8  // Get the Stripe signature
9  const headersList = await headers();
10  const signature = headersList.get("stripe-signature");
11
12  // Getting the raw body
13  const body = await request.text();
14
15  // Constructing the event with raw body, signature, and webhook secret
16  const event = stripe.webhooks.constructEvent(
17    body,
18    signature,
19    process.env.STRIPE_WEBHOOK_SECRET
20  );
21
22  switch (event.type) {
23    // Handling subscription creation
24    case "customer.subscription.created": {
25      await prisma.user.update({
26        where: { stripeCustomerId: event.data.object.customer },
27        data: {
28          subscriptionId: event.data.object.id,
29          subscriptionStatus: event.data.object.status,
30          role:
31            event.data.object.status === "active" || "trialing"
32              ? "paid user"
33              : "free user",
34        },
35      });
36      break;
37    }
38
39    default:
40      console.warn(`Unhandled event type: ${event.type}`);
41  }
42
43  return new Response("Stripe success", {
44    status: 200,
45  });
46}

Stripe test card

Stripe update database