๐Ÿฆ„ Building a pricing page and charging visitors with NextJS ๐Ÿคฏ ๐Ÿคฏ

๐Ÿฆ„ Building a pricing page and charging visitors with NextJS ๐Ÿคฏ ๐Ÿคฏ

ยท

11 min read

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


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

cat


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 />
   </>
   )
}

Pricing


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

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

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

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

Purchase

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

Image description

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

Click on the created event

Image description

Click on "Reveal" on "Signing key",

Reveal

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:

  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.

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.

Notion


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:

  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.


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

After that just add any name and click Submit

Submit

Click on Show and copy the key

Show

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

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

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

Image description

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:

Final results


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

Full Project


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

Image description

Learn How to get GitHub stars

ย