tutorial // Sep 24, 2021

How to Build a Credit Card Form Using Stripe.js with React.js in Next.js

How to create a credit card form using Stripe.js and Stripe Elements as well as how to retrieve that credit card form's value and generate a Stripe source token.

How to Build a Credit Card Form Using Stripe.js with React.js in Next.js

Getting started

For this tutorial, to give us a starting point for our work, we're going to use the CheatCode Next.js Boilerplate. Let's clone a copy from Github now:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate.git

Next, cd into the project and install its dependencies:

Terminal

cd nextjs-boilerplate && npm install

Finally, go ahead and start up the development server:

Terminal

npm run dev

With that, we're ready to get started.

Accessing our Stripe API keys

Before we dig into the code, for this tutorial, we're going to need access to a Stripe account. Head over to the signup page on their site and create an account if you haven't already.

Once you have an account, login to the dashboard. It should look something like this:

O66I0fD25fe8zKhG/qIARNw0m7H7jbEhc.0
View your API keys in the Stripe dashboard.

Where we want to navigate to is the page pictured above. To get there:

  1. In the top-right corner, make sure that you've toggled the "Test mode" toggle so that it's lit up (as of writing this will turn orange when activated).
  2. To the left of that toggle, click the "Developers" button.
  3. On the next page, in the left-hand navigation menu, select the "API keys" tab.
  4. Under the "Standard keys" block on this page, locate your "Publishable key."
  5. Copy this key (don't worry, it's intended to be exposable to the public).

Next, once we have our publishable key, we need to open up the project we just cloned and navigate to the /settings/settings-development.js file:

/settings/settings-development.js

const settings = {
  graphql: { ... },
  meta: { ... },
  routes: { ... },
  stripe: {
    publishableKey: "<Paste your publishable key here>",
  },
};

export default settings;

In this file, alphabetically at the bottom of the exported settings object, we want to add a new property stripe and set it to an object with a single property: publishableKey. For the value of this property, we want to paste in the publishable key you copied from the Stripe dashboard above. Paste it in and then save this file.

In the boilerplate we cloned above, there's a built-in mechanism for loading our settings based on the value of process.env.NODE_ENV (e.g., development, staging, or production). Here, we assume we're developing on our local machine and add our keys to the settings-development.js file. If we were deploying to production, we would make this change in our settings-production.js file, using our live publishable key as opposed to our test publishable key like we see above.

Next, in order to use Stripe in the browser, we need to load the Stripe.js library via the Stripe CDN.

Initializing Stripe.js in the browser

For security purposes, when it comes to hosting the Stripe.js library—what we'll use below to generate our credit card form and retrieve a credit card token with—Stripe does not allow us to self-host. Instead, we need to load the library via a CDN (content delivery network) link, hosted by Stripe.

To load the library, we're going to open up the /pages/_document.js file in our boilerplate which is where Next.js sets up the base HTML template for our site:

/pages/_document.js

import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheet } from "styled-components";

export default class extends Document {
  static async getInitialProps(ctx) { ... }

  render() {
    const { styles } = this.props;

    return (
      <Html lang="en">
        <Head>
          ...
          <script
            src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
            crossOrigin="anonymous"
          ></script>
          <script src="https://js.stripe.com/v3/"></script>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Here, toward the lower middle-half of the <Head></Head> tag we see here (beneath the cdn.jsdelivr.net/npm/bootstrap script), we want to paste in a script tag that points to the CDN-hosted version of Stripe.js: <script src="https://js.stripe.com/v3/"></script>.

This is all we need to do. When we load up our app now, Next.js will load this script tag. When it runs, this script will automatically load Stripe in the browser and give us access to the library via the global variable Stripe.

Writing a script to initialize Stripe

Now that we have access to Stripe itself, next, we need to write a script that will allow us to initialize Stripe with the publishable key that we copied earlier and then easily re-use that initialized copy of the library.

/lib/stripe.js

import settings from "../settings";

const stripe =
  typeof Stripe !== "undefined" ? Stripe(settings.stripe.publishableKey) : null;

export default stripe;

Here, in the /lib folder of the boilerplate we cloned earlier, we're adding a file stripe.js which will pull in our publishableKey that we set in our settings file and then, after checking that the global Stripe variable is defined, call it as a function Stripe(), passing in our publishableKey.

Then, assuming we get back an instance (or null if for some reason Stripe.js fails to load), we export that from our file. As we'll see next, this will allow us to import a "ready to go" copy of Stripe.js without having to rewrite the above code every time we want to access the library (helpful if you're building an app and intend to use Stripe in multiple project files).

Creating a credit card component with Stripe Elements

Now for the fun part. One of the nice parts about using Stripe.js is that it gives us access to their Elements library. This allows us to quickly set up a card form in our app without having to write a lot of boilerplate HTML and CSS. To get started, we're going to set up a class-based component in React.js (this will give us better control over initializing Stripe and Elements than we'd get with a function-based component).

/pages/index.js

import React, { useEffect, useState } from "react";

import StyledIndex from "./index.css";

class Index extends React.Component {
  state = {
    token: "",
    cardError: "",
  };

  componentDidMount() {
    // We'll set up Stripe Elements here...
  }

  handleSubmit = () => {
    // We'll handle token generation for our card here...
  };

  render() {
    const { cardError, token } = this.state;

    return (
      <StyledIndex>
        <div className="credit-card" />
        {cardError && <p className="card-error">{cardError}</p>}
        <button
          onClick={() => this.handleSubmit()}
          className="btn btn-primary btn-lg mt-4"
        >
          Get Token
        </button>
        {token && (
          <div className="mt-4">
            <p className="token">{token}</p>
          </div>
        )}
      </StyledIndex>
    );
  }
}

Index.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Index;

Getting set up, here, we're creating a rough skeleton for the page where we'll render our credit card via Elements. Fortunately, the bulk of the component is quite simple.

Here, we're doing a few things:

  1. Adding the HTML markup that will be used to display our form.
  2. Adding default/placeholder values for two state values that we'll use token and cardError.
  3. Adding placeholder functions for componentDidMount() (where we'll load up Stripe and mount our card form) and handleSubmit() which we'll use to generate our Stripe card token.

Of note, here, we should call quick attention to the <StyledIndex></StyledIndex> component that's wrapping the entirety of our compnent's markup. This is a styled component which is a React component generated by the library styled-components. This library allows us to create custom React components that represent some HTML element (e.g., a <div></div> or a <p></p>) and then attach CSS styles to it.

Let's take a look at the file where that's being imported from real quick:

/pages/index.css.js

import styled from "styled-components";

export default styled.div`
  .credit-card {
    border: 1px solid #eee;
    box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.02);
    padding: 20px;
    border-radius: 3px;
    font-size: 18px;

    &.StripeElement--focus {
      border: 1px solid #ffcc00;
      box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.02);
    }
  }

  .card-error {
    background: #ea4335;
    color: #fff;
    padding: 20px;
    border-radius: 3px;
    margin-top: 10px;
  }

  .token {
    background: #eee;
    padding: 20px;
    border-radius: 3px;
    font-size: 16px;
    color: #444;
  }
`;

Here, we import the object styled from the styled-components library (this is pre-installed in the boilerplate we cloned earlier). On this object, we can find a series of functions named after the standard HTML elements, for example: styled.div(), styled.p(), or styled.section().

For our credit card form, we're going to use a plain <div></div> tag so we're using the styled.div() function here. Though it may not look like it, the styled.div`` part here is equivalent to styled.div(``). The idea being that in JavaScript, if we're going to call a function where the only argument is a string, we can omit the parentheses and replace our single or double-quotes with backticks, passing our string as normal.

In this file, this is purely a syntactic choice to keep our code inline with the examples offered by styled-components and its authors.

Focusing on the contents of the string we're passing to styled.div(), we're just adding a little bit of polish to our card form (by default, Stripe gives us a very stripped down form with no styles). Of note, here, you'll see the StripeElement--focus class having styles applied to it (we use a nested CSS selector with & to say "if the .credit-card element also has the class StripeElement--focus, apply these styles.").

This is an auto-generated class that Stripe automatically applies when a user focuses or "clicks into" our card form. We use this to change the border color of our card form to acknowledge the interaction.

/pages/index.js

import React, { useEffect, useState } from "react";
import stripe from "../lib/stripe";

import StyledIndex from "./index.css";

class Index extends React.Component {
  state = {
    token: "",
    cardError: "",
  };

  componentDidMount() {
    const elements = stripe.elements();

    this.creditCard = elements.create("card", {
      style: {
        base: {
          fontSize: "18px",
        },
      },
    });

    this.creditCard.on("change", (event) => {
      if (event.error) {
        this.setState({ cardError: event.error.message });
      } else {
        this.setState({ cardError: "" });
      }
    });

    this.creditCard.mount(".credit-card");
  }

  handleSubmit = () => {
    // We'll handle token generation for our card here...
  };

  render() {
    const { cardError, token } = this.state;

    return (
      <StyledIndex>
        <div className="credit-card" />
        {cardError && <p className="card-error">{cardError}</p>}
        <button
          onClick={() => this.handleSubmit()}
          className="btn btn-primary btn-lg mt-4"
        >
          Get Token
        </button>
        {token && (
          <div className="mt-4">
            <p className="token">{token}</p>
          </div>
        )}
      </StyledIndex>
    );
  }
}

Index.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Index;

Back in our <Index /> component where we're rendering the markup for our credit card, now we're ready to actually mount our credit card. By "mount," we mean telling Stripe to replace the <div className="credit-card" /> tag on our page with the actual credit card form from Stripe Elements.

Up top, we can see that we're importing the /lib/stripe.js file we set up earlier. Down in our componentDidMount() method, we use this to get access to the .elements() function which creates an instance of the Stripe elements library for us.

Next, in order to "mount" our credit card, we first need to create the element that represents it (think of this like the in-memory representation of the card form before its been "drawn" on screen). To do it, we call to elements.create(), passing in the type of element we want to create as a string "card" as the first argument and then an options object as the second argument.

For the options, we're setting a slightly larger-than-default font size (due to how Stripe mounts our card form, unfortunately, we can't set the font-size with the rest of the CSS in our styled component).

Finally, once our element is created, we store it on our <Index></Index> component class as this.creditCard. This will come in handy later when we need to reference this.creditCard in order to access its value and generate a token.

Below this code, next, in order to "catch" or handle the errors generated by Stripe elements, we need to add an event listener to this.creditCard. To do it, Stripe gives us a .on() method on that instance. This takes the name of the event we want to listen for—here, `"change"—and a callback function to call whenever that event occurs.

For our needs, the only change we care about is if this.creditCard produces an error. Inside of our change callback, this will be available as event.error. If it exists, here, we grab the event.error.message value (text describing the error that's occurring) and set it onto state.

If there's not an error (meaning a previous error was corrected or there was never an error to begin with), we make sure to reset cardError on state to be an empty string.

Finally, beneath this change event handler, we finally get to the point where we mount our Stripe elements form via this.creditCard.mount(). Notice that we pass in the className we set on the <div></div> down in our render() method to this function. This tells Stripe to inject or "mount" the elements form in this spot.

Just beneath this, we can also see that we conditionally render our cardError if it has a value (remember, we styled this up earlier inside of our /pages/index.css.js file).

While this technically gets us a credit card form on page, to finish up, we're going to learn how to access the value typed into our credit card form and convert that into a Stripe source token.

Generating a Stripe token

In order to make our form useful, now, we're going to learn how to generate what's known as a Stripe source token. Because of various laws around the transmission of financial data (e.g., PCI Compliance), offering a credit card form involves a bit more legal complexity than collecting more innocuous forms of data like a name or email address.

Because complying with this sort of regulation is a significant burden on small businesses and independent operators, companies like Stripe step in to solve the problem. They act as a middle man between your customer's credit card data and your servers. Instead of copying credit card data directly to your own server—and thus, having to comply with PCI laws—you hand the data off to Stripe who's servers/code are already PCI compliant (and promise to be in the future).

The mechanism that Stripe uses to manage this process is known as a source token (here, source being a "payment source" like a credit card or bank account). When we use Stripe.js, we establish a secure connection over HTTPS back to Stripe's servers, send them the card data our user inputs, and then Stripe responds with a unique token that represents that credit card. In order to actually charge that card, we pass that unique token along with our other requests to Stripe on our own server. When we do, Stripe "looks up" the actual credit card data associated with that token on their own secure servers/database.

/pages/index.js

import React, { useEffect, useState } from "react";
import stripe from "../lib/stripe";

import StyledIndex from "./index.css";

class Index extends React.Component {
  state = {
    token: "",
    cardError: "",
  };

  componentDidMount() { ... }

  handleSubmit = () => {
    stripe.createToken(this.creditCard).then(({ error, token }) => {
      if (error) {
        this.setState({ cardError: error.message });
      } else {
        this.setState({ token: token.id });
      }
    });
  };

  render() {
    const { cardError, token } = this.state;

    return (
      <StyledIndex>
        <div className="credit-card" />
        {cardError && <p className="card-error">{cardError}</p>}
        <button
          onClick={() => this.handleSubmit()}
          className="btn btn-primary btn-lg mt-4"
        >
          Get Token
        </button>
        {token && (
          <div className="mt-4">
            <p className="token">{token}</p>
          </div>
        )}
      </StyledIndex>
    );
  }
}

Index.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Index;

Back in our <Index></Index> component and focusing on our handleSubmit() method, we call to the stripe.createToken() method, passing in the this.creditCard value we set up earlier. From this, Stripe knows how to retrieve the current input value. Behind the scenes, it takes this value, transmits it to its own servers, and then responds. That response is captured here in the .then() callback (we expect stripe.createToken() to return a JavaScript Promise) here in our code.

To that callback, we expect to get passed an object with a token property on it that is itself an object which has our actual source token stored in its .id property. Here, assuming that the error value also included on this response object is not defined, we take that token.id and set it back onto the state of our component as this.state.token (this.setState() modifies the this.state value on our component).

That's it! At this point, we'd take the token.id we've received and relay it to our own servers to then pass on to Stripe. To test it out, we can enter the card number 4242 4242 4242 4242, passing in any future expiration date and CVC.

Wrapping up

In this tutorial, we learned how to generate a credit card form using the Stripe Elements library bundled inside of Stripe.js. We learned how to include Stripe.js in our HTML and initialize it with our publishable key we obtained from the Stripe dashboard and then import that instance to generate our form. We also learned how to retrieve our user's input via Stripe.js and then pass that to Stripe's .createToken() method to generate a secure card token for use elsewhere in our app.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode