Den Dribbles

Taking Stripe Payments With Next.js 10, TypeScript 4, React 17 and Vercel

November 07, 2020

Over my previous posts, I have been exploring some of the neat things about Next.js 10 and using Vercel as a host.

Today we are going to show how you can build out your next store or start financing your own dreams.

We will use a Stripe + Nextjs starter template, upgrade React to version 17, TypeScript to version 4 and deploy the final working application to Vercel via the CLI.

I tell you what, I sure am ready to leave me 9-5 job.

Prerequisites and requirements

  1. Have a Stripe account.
  2. Have your API keys ready.
  3. A Vercel account.

Getting started

Thanks to create-next-app, we already have an example that we can get started with and explore!

Run the following to create a Next.js example app in the folder with-stripe-typescript-app:

# Create Stripe TypeScript example
npx create-next-app --example with-stripe-typescript with-stripe-typescript-app

Change into the newly-created directory.

Optionally, We are going to make some changes to this template for the sake of bringing TypeScript and React things up to version 4 and 17 respectively!

# Move TypeScript from v3 -> v4, React from v16 -> v17
npm install react@latest react-dom@latest
# Update types
npm install --save-dev typescript@latest @types/react@latest

In my case, this brought TypeScript up to v4.0.5 and React + ReactDOM to v17.0.1. Note that in future, breaking changes could break the app.

You could check for the other packages as well (Stripe, etc.) but I will leave them for the sake of working with what is currently there.

Preparing the app

Before we go too deep into exploration, we need to add in your Stripe API Keys. In my case, I will just use the test keys from the dashboard.

As for the webhook secret, this comes from the Stripe CLI. Follow the instructions on the link to install.

After installing the Stripe CLI, run stripe login and log into your account (this will open up the browser to confirm).

Next, run stripe listen --forward-to localhost:<your-port-likely-3000>/api/webhooks. This will give you back a webhook secret you can use.

From the root of the project, run cp .env.local.example to .env.local.example.

Inside, you’ll fine some environment variables that we need to update:

# Stripe keys
# https://dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<your-stripe-pk>
STRIPE_SECRET_KEY=<your-stripe-sk>
STRIPE_PAYMENT_DESCRIPTION='Software development services'
# https://stripe.com/docs/webhooks/signatures
STRIPE_WEBHOOK_SECRET=<your-webhook-secret-from-stripe-cli>

Exploring the app

To start the app, run npm run dev in the terminal.

Head to http://localhost:3000 and you’ll be faced with the following screen:

Localhost running

Localhost running

This page allows us to run three examples:

  1. Donation using Stripe Checkout.
  2. Donation using Stripe Elements.
  3. Using the Shopping Cart.

Let’s explore the code for each page.

Donate with Stripe Checkout

Checking out the first option with Donation with Checkout.

The code for how this page works can be found at pages/donate-with-checkout.tsx.

import { NextPage } from "next"
import Layout from "../components/Layout"

import CheckoutForm from "../components/CheckoutForm"

const DonatePage: NextPage = () => {
  return (
    <Layout title="Donate with Checkout | Next.js + TypeScript Example">
      <div className="page-container">
        <h1>Donate with Checkout</h1>
        <p>Donate to our project 💖</p>
        <CheckoutForm />
      </div>
    </Layout>
  )
}

export default DonatePage

All the future examples can be found at pages/path/to/route. I won’t be touching deeper on the page entry points moving further.

Within that form, we can see the checkout form in components/CheckoutForm.

While I won’t go too deep into each and every piece of information here, if we look at the code and see the handleSubmit closure function, we can see where most the important information comes from:

const handleSubmit: React.FormEventHandler<HTMLFormElement> = async e => {
  e.preventDefault()
  setLoading(true)
  // Create a Checkout Session.
  const response = await fetchPostJSON("/api/checkout_sessions", {
    amount: input.customDonation,
  })

  if (response.statusCode === 500) {
    console.error(response.message)
    return
  }

  // Redirect to Checkout.
  const stripe = await getStripe()
  const { error } = await stripe!.redirectToCheckout({
    // Make the id field from the Checkout Session creation API response
    // available to this file, so you can provide it as parameter here
    // instead of the {{CHECKOUT_SESSION_ID}} placeholder.
    sessionId: response.id,
  })
  // If `redirectToCheckout` fails due to a browser or network
  // error, display the localized error message to your customer
  // using `error.message`.
  console.warn(error.message)
  setLoading(false)
}

This code re-directs us to the Stripe Checkout where we can pay with some test cards.

The API call code it uses to authorize the checkout session comes from pages/api/checkout_sessions/*.ts where * refers to any of the three files that help out here.

If we run through the UI and click on “donate” we can see that in action.

Stripe Checkout

Stripe Checkout

Using a Stripe test card we can pop in 4242 4242 4242 4242 for the card number, any 3 digits for the CVC and any future date for validating the purchase.

If we check our terminal, we will see some neat confirmation logs for both the Nextjs app logs and webhook logs:

# Nextjs App logs
✅ Success: evt_1Hke6eJV8JMnC8XlhOPbQAy2
💵 Charge id: ch_1Hke6dJV8JMnC8XltpI9mM15
✅ Success: evt_1Hke6eJV8JMnC8Xlm2EtO9Cq
🤷‍♀️ Unhandled event type: payment_method.attached
✅ Success: evt_1Hke6eJV8JMnC8XlpWp04upc
🤷‍♀️ Unhandled event type: customer.created
✅ Success: evt_1Hke6fJV8JMnC8Xl29TUZxrT
💰 PaymentIntent status: succeeded
✅ Success: evt_1Hke6fJV8JMnC8XlcTKuetWs
🤷‍♀️ Unhandled event type: checkout.session.completed
# Stripe Webhook Logs
2020-11-07 10:04:59  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:08:46   --> payment_intent.created [evt_id]
2020-11-07 10:08:46  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:12:33   --> charge.succeeded [evt_id]
2020-11-07 10:12:33  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:12:34   --> payment_method.attached [evt_1Hke6eJV8JMnC8Xlm2EtO9Cq]
2020-11-07 10:12:34  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:12:34   --> customer.created [evt_1Hke6eJV8JMnC8XlpWp04upc]
2020-11-07 10:12:34  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:12:34   --> payment_intent.succeeded [evt_1Hke6fJV8JMnC8Xl29TUZxrT]
2020-11-07 10:12:34  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]
2020-11-07 10:12:34   --> checkout.session.completed [evt_1Hke6fJV8JMnC8XlcTKuetWs]
2020-11-07 10:12:34  <--  [200] POST http://localhost:3000/api/webhooks [evt_id]

The UI itself will redirect to /result where the response JSON body will be shown.

Response on result page

Response on result page

Donate with Stripe Elements

If we return to the home page and select the Donate with Elements option, we will come to a similar donation page but with the difference that we are now using Stripe Elements from the @stripe/react-stripe-js package.

This enables you to make purchase attempts directly from within your website and has flexible styling options to keep it within your styling.

Similar to before, it is the handleSubmit closure function from the components/ElementsForm.tsx form that will give us the most information on what is happening:

const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault()
    // Abort if form isn't valid
    if (!e.currentTarget.reportValidity()) return
    setPayment({ status: 'processing' })

    // Create a PaymentIntent with the specified amount.
    const response = await fetchPostJSON('/api/payment_intents', {
      amount: input.customDonation,
    })
    setPayment(response)

    if (response.statusCode === 500) {
      setPayment({ status: 'error' })
      setErrorMessage(response.message)
      return
    }

    // Get a reference to a mounted CardElement. Elements knows how
    // to find your CardElement because there can only ever be one of
    // each type of element.
    const cardElement = elements!.getElement(CardElement)

    // Use your card Element with other Stripe.js APIs
    const { error, paymentIntent } = await stripe!.confirmCardPayment(
      response.client_secret,
      {
        payment_method: {
          card: cardElement!,
          billing_details: { name: input.cardholderName },
        },
      }
    )

    if (error) {
      setPayment({ status: 'error' })
      setErrorMessage(error.message ?? 'An unknown error occured')
    } else if (paymentIntent) {
      setPayment(paymentIntent)
    }
  }

The API call code it uses to authorize the checkout session comes from pages/api/payment_intents/*.ts where * refers to any of the three files that help out here.

If we fill out the details similar to before with the same test card and click Donate $x, you will see that the entire payment process happens on the page as opposed to running through the redirects.

Elements form

Elements form

Elements paid

Elements paid

Exploring the Shopping Cart

As for the final option of Use Shopping Cart, you’ll see the example gives us a lovely set of options to add items to a cart, then checkout.

This actually works by using a package called use-shopping-cart which is a delightful package that helps maintain a Stripe shopping cart using React hooks and the related stock keeping units (SKUs) used in Stripe.

In real-world usage, you can add the products through the Stripe Dashboard.

This example follows a similar route to the basic checkout example. If you add some items to the cart and checkout, you will be redirected to Stripe Checkout to complete to order and follow a similar redirect path.

Shopping cart page

Shopping cart page

The difference in this cart, however, is that your items will show on the checkout page!

Checkout with shopping cart

Checkout with shopping cart

See the payments in the dashboard

If you head to the Payments section of your Stripe Dashboard, you will be able to confirm the test payments we made.

Payments in the dashboard

Payments in the dashboard

Deploying to Vercel

If you have not already, install Vercel using npm i -g vercel.

Now, simply run vercel from the root of the application and follow the prompts.

The above requires you to have a Vercel account setup.

Once this is done, you will have a link to the live website! The battle, however, is not over yet. If you try to run the checkout from here, your Developer Tools console will inform you that the API keys are not set.

We need to now add our keys to Vercel.

Creating the webhook in Stripe

In order to capture webhooks in our production app, we need to create a webhook URL in the Stripe Dashboard. It will look something like https://your-url.your-account.vercel.app/api/webhooks.

Creating a production webhook

Creating a production webhook

Once created in the dashboard, grab the Signing Secret and get ready to add this to Vercel.

Adding Environment Variables

There will be a link to the settings for your application given back on terminal (something like https://vercel.com/[your-account]/[app-name]/settings).

Head to this link and select Environment Variables from the sidebar. From here, we want to add back in our variables using what we had from .env.local in our project, only replace the STRIPE_WEBHOOK_SECRET with the actual link we created in the section before.

Once these values have been filled out, run vercel --prod from the terminal once again from the root directory of the repo to re-deploy the app so that the new environment variables take place.

Head to your URL now, run through one of the flows we described during this post and you are done. Congratulations!

Production Example Payment

Production Example Payment

Production payment on the Stripe Dashboard

Production payment on the Stripe Dashboard

Conclusion

In summary, we have looked today at setting up React 17 and TypeScript 4.x in a Nextjs 10 application and event taken it to production using Vercel to host the live website with the ability to accept payments.

Most of these example API routes and implementations from the app can be re-used in the real-world for your own projects.

Stripe + Vercel = Greatness. Happy hacking!

Resources and further reading

  1. Base Stripe repo
  2. Forked repo
  3. Locating API keys in Stripe
  4. Stripe API Keys link
  5. Stripe test cards
  6. Vercel
  7. Production URL
  8. Completed Project

Image credit: Sam Dan Truong

Originally posted on my blog. Follow me on Twitter for more hidden gems @dennisokeeffe92.


A personal blog on all things of interest. Written by Dennis O'Keeffe, Follow me on Twitter