Table of contents
- TL;DR
- Can you help me out? โค๏ธ
- Let's set it up ๐ฅ
- Setting up your payment provider ๐ค ๐ฐ
- Sending users to the checkout page ๐
- Notion blew my mind ๐คฏ
- Processing the purchase request and adding the leads to Notion ๐๐ปโโ๏ธ
- Setting up notion โ๐ป
- You nailed it ๐
- Can you help me out? โค๏ธ
TL;DR
In this article, we will build a page with multiple pricing tiers.
Visitors can press the "Purchase" button and go to a checkout page.
Once completed, we will send the customers to a success page and save them into our database.
This use case can be helpful in:
Purchase a course
Purchase a subscription
Purchase a physical item
Donate money
Buy you a coffee ๐
And so many more.
Can you help me out? โค๏ธ
I love making open-source projects and sharing them with everybody.
If you could help me out and star the project โญ๏ธ I would be super, super grateful โค๏ธ
(this is also the source code of this tutorial) ๐
https://github.com/github-20k/growchief
Let's set it up ๐ฅ
Let's start by creating a new NextJS project:
npx create-next-app@latest
Just click enter multiple times to create the project.
I am not a big fan of Next.JS's new App router - so I will use the old pages
folder, but feel free to do it your way.
Let's go ahead and add the pricing packages.
Let's make a new folder called components and add our pricing component.
mkdir components
cd components
touch pricing.component.tsx
And add the following content:
export const PackagesComponent = () => {
return (
<div className="mt-28">
<h1 className="text-center text-6xl max-sm:text-5xl font-bold">
Packages
</h1>
<div className="flex sm:space-x-4 max-sm:space-y-4 max-sm:flex-col">
<div className="flex-1 text-xl mt-14 rounded-xl border border-[#4E67E5]/25 bg-[#080C23] p-10 w-full">
<div className="text-[#4d66e5]">Package one</div>
<div className="text-6xl my-5 font-light">$600</div>
<div>
Short description
</div>
<button
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#4E67E5] text-xl max-sm:text-lg hover:bg-[#8a9dfc] transition-all"
>
Purchase
</button>
<ul>
<li>First feature</li>
<li>Second feature</li>
</ul>
</div>
<div
className="flex-1 text-xl mt-14 rounded-xl border border-[#9966FF]/25 bg-[#120d1d] p-10 w-full"
>
<div className="text-[#9967FF]">Package 2</div>
<div className="text-6xl my-5 font-light">$1500</div>
<div>
Short Description
</div>
<button
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#9966FF] text-xl max-sm:text-lg hover:bg-[#BB99FF] transition-all"
>
Purchase
</button>
<ul>
<li>First Feature</li>
<li>Second Feature</li>
<li>Thired Feature</li>
</ul>
</div>
<div
className="flex-1 text-xl mt-14 rounded-xl border border-[#F7E16F]/25 bg-[#19170d] p-10 w-full"
>
<div className="text-[#F7E16F]">Package 3</div>
<div className="text-6xl my-5 font-light">$1800</div>
<div>
Short Description
</div>
<button
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#F7E16F] text-xl max-sm:text-lg hover:bg-[#fdf2bb] transition-all"
>
Purchase
</button>
<ul>
<li>First Feature</li>
<li>Second Feature</li>
<li>Thired Feature</li>
<li>Fourth Feature</li>
<li>Fifth Feature</li>
</ul>
</div>
</div>
</div>
);
};
This is a very simple component with Tailwind (CSS) to show three types of packages ($600, $1500, and $1800). Once clicked on any of the packages, we will move the visitor to a purchase page where they can purchase the package.
Go to the root dir and create a new index page (if it doesn't exist)
cd pages
touch index.tsx
Add the following code to the file:
import React from 'react';
import {PackagesComponent} from '../components/pricing.component';
const Index = () => {
return (
<>
<PackagesComponent />
</>
)
}
Setting up your payment provider ๐ค ๐ฐ
Most payment providers work in the same way.
Send an API call to the payment provider with the amount you want to charge and the success page to send the user after the payment.
You get a URL back from the API call with a link to the checkout page and redirect the user (user leaving your website).
Once the purchase is finished, it will redirect the user to the success page.
The payment provider will send an API call to a specific route you choose to let you know the purchase is completed (asynchronically)
I use Stripe - it is primarily accessible everywhere, but feel free to use your payment provider.
Head over to Stripe, click on the developer's tab, move to "API Keys," and copy the public and secret keys from the developer's section.
Go to the root of your project and create a new file called .env
and paste the two keys like that:
PAYMENT_PUBLIC_KEY=pk_test_....
PAYMENT_SECRET_KEY=sk_test_....
Remember that we said Stripe would inform us later about a successful payment with an HTTP request?
Well... we need to
Set the route to get the request from the payment
Protect this route with a key
So while in the Stripe dashboard, head to "Webhooks" and create a new webhook.
You must add an "Endpoint URL". Since we run the project locally, Stripe can only send us a request back if we create a local listener or expose our website to the web with ngrok.
I prefer the ngrok option because, for some reason, the local listener didn't always work for me (sometimes send events, sometimes not).
So while your Next.JS project runs, just run the following commands.
npm install -g ngrok
ngrok http 3000
And you will see Ngrok serves your website in their domain. Just copy it.
And paste it into the Stripe webhook "Endpoint URL," also adding the path to complete the purchase /api/purchase
After that, click "Select Events."
Choose "checkout.session.async_payment_succeeded" and "checkout.session.completed"
Click "Add event" and then "Add endpoint."
Click on the created event
Click on "Reveal" on "Signing key",
copy it and open .env
, and add
PAYMENT_SIGNING_SECRET=key
Sending users to the checkout page ๐
Let's start by installing Stripe and also some types (since I am using typescript)
npm install stripe --save
npm install -D stripe-event-types
Let's create a new API route to create a checkout URL link for our users, depending on the price.
cd pages/api
touch prepare.tsx
Here is the content of the file:
import type { NextApiRequest, NextApiResponse } from 'next'
const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({error: "Method not allowed"});
}
if (!req.query.price || +req.query.price <= 0) {
return res.status(400).json({error: "Please enter a valid price"});
}
const { url } = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "USD",
product_data: {
name: "GrowChief",
description: `Charging you`,
},
unit_amount: 100 * +req.query.price,
},
quantity: 1,
},
],
mode: "payment",
success_url: "http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "http://localhost:3000",
});
return req.json({url});
}
Here is what's going on here:
We set a new Stripe instance with the SECRET key from our
.env
file.We make sure the
METHOD
of the route isGET.
We check that we get a query string of
price
higher than 0.We make a Stripe call to create a Stripe checkout URL. We purchased 1 item; you can probably see that the
unit_amount
is multiplied by 100. If we send 1, it would be $0.01; multiplied by a hundred will make it $1.We send the
URL
back to the client.
Let's open back our packages.component.tsx
component and add the api call.
const purchase = useCallback(async (price: number) => {
const {url} = await (await fetch(`http://localhost:3000/api/prepare?price=${price}`)).json();
window.location.href = url;
}, []);
And for the full code of the page
export const PackagesComponent = () => {
const purchase = useCallback(async (price: number) => {
const {url} = await (await fetch(`http://localhost:3000/api/prepare?price=${price}`)).json();
window.location.href = url;
}, []);
return (
<div className="mt-28">
<h1 className="text-center text-6xl max-sm:text-5xl font-bold">
Packages
</h1>
<div className="flex sm:space-x-4 max-sm:space-y-4 max-sm:flex-col">
<div className="flex-1 text-xl mt-14 rounded-xl border border-[#4E67E5]/25 bg-[#080C23] p-10 w-full">
<div className="text-[#4d66e5]">Package one</div>
<div className="text-6xl my-5 font-light">$600</div>
<div>
Short description
</div>
<button onClick={() => purchase(600)}
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#4E67E5] text-xl max-sm:text-lg hover:bg-[#8a9dfc] transition-all"
>
Purchase
</button>
<ul>
<li>First feature</li>
<li>Second feature</li>
</ul>
</div>
<div
className="flex-1 text-xl mt-14 rounded-xl border border-[#9966FF]/25 bg-[#120d1d] p-10 w-full"
>
<div className="text-[#9967FF]">Package 2</div>
<div className="text-6xl my-5 font-light">$1500</div>
<div>
Short Description
</div>
<button onClick={() => purchase(1200)}
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#9966FF] text-xl max-sm:text-lg hover:bg-[#BB99FF] transition-all"
>
Purchase
</button>
<ul>
<li>First Feature</li>
<li>Second Feature</li>
<li>Thired Feature</li>
</ul>
</div>
<div
className="flex-1 text-xl mt-14 rounded-xl border border-[#F7E16F]/25 bg-[#19170d] p-10 w-full"
>
<div className="text-[#F7E16F]">Package 3</div>
<div className="text-6xl my-5 font-light">$1800</div>
<div>
Short Description
</div>
<button onClick={() => purchase(1800)}
className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#F7E16F] text-xl max-sm:text-lg hover:bg-[#fdf2bb] transition-all"
>
Purchase
</button>
<ul>
<li>First Feature</li>
<li>Second Feature</li>
<li>Thired Feature</li>
<li>Fourth Feature</li>
<li>Fifth Feature</li>
</ul>
</div>
</div>
</div>
);
};
We have added onClick
on each button of the page with the right price to create the Checkout page.
Notion blew my mind ๐คฏ
Notion is an excellent tool for knowledge & documentation.
I have been working for Novu for over a year and used Notion primarily for our team.
If you have ever played with Notion, you have probably noticed they have a slick editor - one of the best I have ever played with (at least for me).
I HAVE REALIZED YOU CAN USE NOTION CONTENT WITH AN API.
I opened a notion-free account and went out to check their pricing - I was sure they would not offer API for their free tier; I was very wrong, they do, and it's super fast.
Their most significant limitation is that they let you make a maximum of 3 requests per second - but that's not a big problem if you cache your website - aka getStaticProps.
Processing the purchase request and adding the leads to Notion ๐๐ปโโ๏ธ
Remember we set a webhook for Stripe to send us a request once payment is completed?
Let's build this request, validate it, and add the customer to Notion.
Since the request is not a part of the user journey and sits on a different route, it's exposed to the public.
It means that we have to protect this route - Stripe offers a great way to validate it with Express, but since we are using NextJS, we need to modify it a bit, so let's start by installing Micro.
npm install micro@^10.0.1
And open a new route for the purchase:
cd pages
touch purchase.tsx
Open it up and add the following code:
/// <reference types="stripe-event-types" />
import Stripe from "stripe";
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any);
export const config = { api: { bodyParser: false } };
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const signature = req.headers["stripe-signature"] as string;
const reqBuffer = await buffer(req);
const event = stripe.webhooks.constructEvent(
reqBuffer,
signature,
process.env.PAYMENT_SIGNING_SECRET!
) as Stripe.DiscriminatedEvent;
if (
event.type !== "checkout.session.async_payment_succeeded" &&
event.type !== "checkout.session.completed"
) {
res.json({invalid: true});
return;
}
if (event?.data?.object?.payment_status !== "paid") {
res.json({invalid: true});
return;
}
/// request is valid, let's add it to notion
}
This is the code to validate the request; let's see what's going on here:
We start by import typing (remember we installed
stripe-event-types
before).We set a new Stripe instance with our secret key.
We tell the route not to parse it into JSON because Stripe sends us the request in a different format.
We extract the
stripe-signature
from the header and use theconstructEvent
function to validate the request and tell us the event Stripe sent us.We check that we get the event
checkout.session.async_payment_succeeded
; if we get anything else, we ignore the request.If we succeeded, but the customer didn't pay, we also ignored the request.
We have a place to write the logic of the purchase.
After this part, this is your chance to add your custom logic; it could be any of the:
Register the user to a newsletter
Register the user to the database
Activate a user subscription
Send the user a link with the course URL
And so many more.
For our case, we will add the user to Notion.
Setting up notion โ๐ป
Before playing with Notion, let's create a new Notion integration.
Head over to "My integrations."
https://www.notion.so/my-integrations
And click "New Integration"
After that just add any name and click Submit
Click on Show and copy the key
Head over to your .env file and add the new key
NOTION_KEY=secret_...
Let's head over to notion and create a new Database
This database won't be exposed to the API unless we specify that, so click on the "..." and then "Add connections" and click the newly created integration.
Once that is done, copy the ID of the database and add it to your .env
file.
NOTION_CUSTOMERS_DB=your_id
Now you can play with the field in the database any way you want.
I will stick with the "Name" field and add the customer's name from the Stripe purchase.
Not let's install notion client by running
npm install @notionhq/client --save
Let's write the logic to add the customer's name to our database.
import { Client } from "@notionhq/client";
const notion = new Client({
auth: process.env.NOTION_KEY,
});
await notion.pages.create({
parent: {
database_id: process.env.NOTION_CUSTOMERS_DB!,
},
properties: {
Name: {
title: [
{
text: {
content: event?.data?.object?.customer_details?.name,
},
},
],
},
},
});
This code is pretty straightforward.
We set a new Notion instance with the Notion secret key and then create a new row in our database with the prospect's name.
And the full purchase code:
/// <reference types="stripe-event-types" />
import Stripe from "stripe";
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import { Client } from "@notionhq/client";
const notion = new Client({
auth: process.env.NOTION_KEY,
});
const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any);
export const config = { api: { bodyParser: false } };
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const signature = req.headers["stripe-signature"] as string;
const reqBuffer = await buffer(req);
const event = stripe.webhooks.constructEvent(
reqBuffer,
signature,
process.env.PAYMENT_SIGNING_SECRET!
) as Stripe.DiscriminatedEvent;
if (
event.type !== "checkout.session.async_payment_succeeded" &&
event.type !== "checkout.session.completed"
) {
res.json({invalid: true});
return;
}
if (event?.data?.object?.payment_status !== "paid") {
res.json({invalid: true});
return;
}
await notion.pages.create({
parent: {
database_id: process.env.NOTION_CUSTOMERS_DB!,
},
properties: {
Name: {
title: [
{
text: {
content: event?.data?.object?.customer_details?.name,
},
},
],
},
},
});
res.json({success: true});
}
You should have something like this:
You nailed it ๐
That's all.
You can find the entire source code here:
https://github.com/github-20k/growchief
You will find more stuff there, such as
Displaying DEV.TO analytics
Collecting information from Notion and displaying it on the website (CMS style)
Entire purchase flow
Can you help me out? โค๏ธ
I hope this tutorial was helpful for you ๐
Any star you can give me would help me tremendously
https://github.com/github-20k/growchief
Learn How to get GitHub stars