The next component for this checkout logic is the API endpoint that will be handling the request sent by the StripeButton
.
Remember that the request is sent to:
1const response = await fetch(`/api/stripe/${plan}/checkout`, {. . .});
Make sure our API endpoint is designed with the right structure:
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 ├── resume
10 └── webhook
Creating new stripe instance
There are a few things we need to do in this route handler. First, we need to initialize a new Stripe instance using the STRIPE_SECRET_KEY
we saved into our .env
file before:
api/stripe/subscribe/monthly/route.js
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 . . .
9}
Checking if user is signed in
And then, you need to test if the user is signed in, and if not, return a response with an error code and a message saying that the user is not signed in.
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 . . .
22}
This message will be sent to the frontend, and displayed inside our StripeButton
:
components/stripeButton.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 const data = await response.json();
24 if (response.ok) {
25 window.open(data.url, "_target");
26 } else {
27 setMessage(data.message);
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}
After the response is returned, the route handler will stop there, and the rest of the handler will not be executed.
Creating checkout session
And then, we can go ahead and create a checkout session using stripe.checkout.sessions.create()
.
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 . . .
9
10 // Create a Stripe checkout session
11 const checkoutSession = await stripe.checkout.sessions.create({
12 mode: "subscription",
13 customer: session?.user?.stripeCustomerId,
14 line_items: [
15 {
16 price: process.env.STRIPE_PRICE_KEY_MONTH,
17 quantity: 1,
18 },
19 ],
20
21 success_url: "http://localhost:3001/",
22 cancel_url: "http://localhost:3001/",
23 });
24
25 . . .
26}
Stripe needs some information from us in order for the checkout session to be created successfully:
mode
Accepts subscription
, payment
and setup
.
subscription
is for recurring payments, which is the option we are going with in this case. payment
is for onetime payment, and setup
is for payments that are not required right now, we are only saving the payment details to charge the customer later.
customer
This is where we provide the user's stripeCustomerId
, so that Stripe can locate the user in their system.
line_items
line_items
contains a list of items that the customer will be purchasing. Inside this parameter, we can provide an array of price
and quantity
.
In our case, we only need one price
, the STRIPE_PRICE_KEY_MONTH
, and the quantity
is 1.
success_url
If the payment is processed successfully, Stripe will redirect the user to success_url
.
cancel_url
If the user cancels the payment, Stripe will redirect to cancel_url
.
Checkout session failure
stripe.checkout.sessions.create()
will return a response, which contains a url
key. If url
is empty, that means something went wrong, and an error response should be returned.
1import Stripe from "stripe";
2import { auth } from "@/libs/auth";
3
4export async function POST() {
5 . . .
6
7 // Create a Stripe checkout session
8 const checkoutSession = await stripe.checkout.sessions.create({. . .});
9
10 // If failed to create checkout session
11 if (!checkoutSession.url) {
12 return Response.json(
13 {
14 message: "Could not create checkout session.",
15 },
16 { status: 500 }
17 );
18 }
19
20 . . .
21}
Again, this message
will be transferred to StripeButton
and displayed to the user.
Checkout session success
And if the url
key is not empty, a success response should be returned.
1import Stripe from "stripe";
2import { auth } from "@/libs/auth";
3
4export async function POST() {
5 . . .
6
7 // If failed to create checkout session
8 if (!checkoutSession.url) {
9 return Response.json(
10 {
11 message: "Could not create checkout session.",
12 },
13 { status: 500 }
14 );
15 }
16
17 // If checkout session created successfully, send the checkout session url in response
18 return Response.json(
19 {
20 url: checkoutSession.url,
21 message: "Checkout session created successfully.",
22 },
23 {
24 status: 200,
25 }
26 );
27}
Make sure the url
is included in the response. As we've discussed in StripeButton
, it will be used to open a new browser tab:
components/stripeButton.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 const data = await response.json();
24 if (response.ok) {
25 window.open(data.url, "_target");
26 } else {
27 setMessage(data.message);
28 }
29 }
30
31 return <>. . .</>;
32}
Testing the Stripe button
Finally, let's put everything together and test if our pricing page works properly. Go to http://localhost:3001/pricing
and make sure you are NOT signed in.
Click on the Get this plan button, and an error message should be displayed.
If you can see the error message, that means the error handling mechanism works. Next, let's try again, but this time, make sure you are signed in, and then click on the Get this plan button.
The button should turn into loading state, and after a few seconds, a new browser tab should be opened with the Stripe Checkout page.
Debugging and troubleshooting
As you can see, it requires several different parts to work together in order for this function to work, which means it is easy for you to encounter bugs. To help you debug, try to log the error messages, variables and responses in the console and see if you are getting the desired result. For example, in our route handler:
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 . . .
9
10 // Create a Stripe checkout session
11 const checkoutSession = await stripe.checkout.sessions.create({
12 mode: "subscription",
13 customer: session?.user?.stripeCustomerId,
14 line_items: [
15 {
16 price: process.env.STRIPE_PRICE_KEY_MONTH,
17 quantity: 1,
18 },
19 ],
20
21 success_url: "http://localhost:3001/",
22 cancel_url: "http://localhost:3001/",
23 });
24
25 . . .
26}
We are supposed to create a new Stripe checkout session, but sometimes there might be issues in the code causing this operation to fail. In this case, use the try catch syntax to catch the error and log it to the console.
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 . . .
9
10 // Create a Stripe checkout session
11 let checkoutSession;
12
13 try {
14 checkoutSession = await stripe.checkout.sessions.create({
15 mode: "subscription",
16 customer: session?.user?.stripeCustomerId,
17 line_items: [
18 {
19 price: process.env.STRIPE_PRICE_KEY_MONTH,
20 quantity: 1,
21 },
22 ],
23
24 success_url: "http://localhost:3001/",
25 cancel_url: "http://localhost:3001/",
26 });
27 } catch (err) {
28 console.log(err);
29 }
30
31 . . .
32}
And now when something goes wrong, the error message from Stripe will be caught and logged, and you will have more information to troubleshoot your code.
We left our error handling in this demo project for simplicity, but you should implement them throughout your project.