Creating a product
Creating a price
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}
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}