🦄 Building a pricing page with NextJS 🤯 🤯
Tags: webdev, javascript, beginners, programming
Published: 2023-07-04 11:34:40

# 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.

![pricing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jfx6dei9v455cqvl63xc.gif)

<hr />
## 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

![stargif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odgdta25mevfop2p55jj.gif)


![cat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hibo09551nkq1rjqqc0u.gif)

<hr />

## Let's set it up 🔥
Let's start by creating a new NextJS project:

```bash
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.

```bash
mkdir components
cd components
touch pricing.component.tsx
```

And add the following content:

```jsx
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)

```bash
cd pages
touch index.tsx
```

Add the following code to the file:
```jsx
import React from 'react';
import {PackagesComponent} from '../components/pricing.component';

const Index = () => {
   return (
   <>
     <PackagesComponent />
   </>
   )
}
```

![Pricing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9774hi46exoftfn1v9s.png)

<hr />

## Setting up your payment provider 🤑 💰

Most payment providers work in the same way.
1. 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.

2. You get a URL back from the API call with a link to the checkout page and redirect the user (user leaving your website).

3. Once the purchase is finished, it will redirect the user to the success page.

4. 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.


![Stripe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ahbetoqimaa15oztld4.gif)

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
1. Set the route to get the request from the payment
2. Protect this route with a key

So while in the Stripe dashboard, head to "Webhooks" and create a new webhook.


![Create](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gbnc61f9zel6ugw81ybk.png)

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.

![Ngrok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cptzpx8csfv8yl7qu3hy.png)

And paste it into the Stripe webhook "Endpoint URL," also adding the path to complete the purchase `/api/purchase`


![Purchase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1f5c7zpp3lz71illhrl.png)

After that, click "Select Events."
Choose "checkout.session.async_payment_succeeded" and "checkout.session.completed"

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72arilu2c5d1etda4h02.png)

Click "Add event" and then "Add endpoint."

Click on the created event

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p9qvfs61mnuj253hjkio.png)

Click on "Reveal" on "Signing key",


![Reveal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxmfh8gadluplcx8tkmr.png)

copy it and open `.env`, and add

```
PAYMENT_SIGNING_SECRET=key
```

<hr />

## Sending users to the checkout page 🚀

Let's start by installing Stripe and also some types (since I am using typescript)

```bash
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.

```bash
cd pages/api
touch prepare.tsx
```

Here is the content of the file:
```javascript
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:
1. We set a new Stripe instance with the SECRET key from our `.env` file.
2. We make sure the `METHOD` of the route is `GET.`
3. We check that we get a query string of `price` higher than 0.
4. 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.
5. We send the `URL` back to the client.

### Let's open back our `packages.component.tsx` component and add the api call.

```jsx
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
```jsx
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.
<hr />

## Notion blew my mind 🤯
Notion is an excellent tool for knowledge & documentation.

I have been working for [Novu](https://novu.co) 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.`

![Notion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkmddo3xq9kzpmti9s8l.png)


<hr />

## 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.

```bash
npm install micro@^10.0.1
```

And open a new route for the purchase:
```bash
cd pages
touch purchase.tsx
```

Open it up and add the following code:
```javascript
/// <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:
1. We start by import typing (remember we installed `stripe-event-types` before).
2. We set a new Stripe instance with our secret key.
3. We tell the route not to parse it into JSON because Stripe sends us the request in a different format.
4. We extract the `stripe-signature` from the header and use the `constructEvent` function to validate the request and tell us the event Stripe sent us.
5. We check that we get the event `checkout.session.async_payment_succeeded`; if we get anything else, we ignore the request.
6. If we succeeded, but the customer didn't pay, we also ignored the request.
7. 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.

<hr />

## 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"

![Integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ntk0p6zdx92wblfp1e3.png)

After that just add any name and click Submit

![Submit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6ixiv4j1h9dvfxtp6t1.png)

Click on Show and copy the key

![Show](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g63t7hha9mphqhx9lut4.png)

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

![Database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02xza48ih0t0cvk7fwlg.gif)

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.

![Add integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mjvl2ni2mpdw5qbmniac.gif)

Once that is done, copy the ID of the database and add it to your `.env` file.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uf3i2zg3g5mv0a8ggovd.png)

```
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

```bash
npm install @notionhq/client --save
```

Let's write the logic to add the customer's name to our database.

```javascript
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:
```javascript
/// <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:

![Final results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t471iju2tn0iw7rp90e6.png)

<hr />

## You nailed it 🚀
That's all.
You can find the entire source code here:
[https://github.com/github-20k/growchief](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


![Full Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62f1r6hz47xeawpvos26.gif)

<hr />

## 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

![stargif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odgdta25mevfop2p55jj.gif)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02tu5o2fw9nagl3j4opk.gif)