DEV Community

Jamie Barton
Jamie Barton

Posted on • Edited on

Working with Stripe Payment Intents and Next.js

On September 14, 2019, Stripe introduced a API for handling payments to comply with new European legislation. If you are already using Stripe, you've probably already heard of or even implemented Payment Intents.

Companies who don't follow the new changes may see transactions being declined by the bank, leaving customers in the dark and reduce sales. πŸ“‰

If your business is based in the European Economic Area or process payments for anybody who is, serve customers in the EEA and accept credit or debit cards, you will need to comply with the Strong Customer Authentication.

Due to the change in processing payments, it's not longer possible to just create payments on the frontend like you were used to previously. There now has to be a server element to creating payments with Stripe.

In this guide, we'll explore how you can start using the new Stripe Payment Intents API with Next.js and follow best practices as set out by Stripe and the industry.

TLDR; Get the code


Prerequisites

  • A Stripe account
  • Stripe Secret key and Publishable key
  • Some knowledge of React & Next.js

If you'd rather follow along with the video format, I recorded a short video covering this post;


Get started

Let's use the Next.js CLI to create a new project and boilerplate.



npm init next-app # or yarn create next-app


Enter fullscreen mode Exit fullscreen mode

Give your project a name and once the dependencies are installed, cd into your project folder.

Now, create the file: pages/checkout.js and add the following;



const CheckoutPage = props => (
  <pre>{JSON.stringify(props, null, 2)}</pre>
);

export default CheckoutPage;


Enter fullscreen mode Exit fullscreen mode

If you now run npm run dev (or yarn dev) you'll see we have an empty props object rendered on the page.

⚠️ Stripe recommends we create a Payment Intent as soon as the amount is known and the customer begins the checkout flow. Payment Intents can also be retrieved if a user is returning to your site at a later date to complete a checkout for the same cart or order they were previously.

Now let's move on... and do just that! πŸ‘―β€β™€οΈ

Server side

Next.js 9.3.0 introduced a the new getServerSideProps lifecycle method we can use to perform server side only behaviour.

This means we no longer need to create an API route to handle creating an intent and handle it directly inside our checkout page.

Let's start by installing the Stripe.js dependency;



npm install stripe # or yarn add stripe


Enter fullscreen mode Exit fullscreen mode

Then inside our pages/checkout.js page, import the Stripe package at the top of our file;



import Stripe from "stripe";


Enter fullscreen mode Exit fullscreen mode

Now to hook into the getServerSideProps method, we must export it as a const.

Inside that method is where we will create our Payment Intent. For the purposes of this tutorial, we'll fix the amount and currency values but inside this method is where I'd recommend making a fetch request to lookup your cart total. Unless your use case permits users to provide their own amount and currency πŸ˜€.

⚠️ Make sure you head to your Developer API Keys Dashboard and copy your Secret key for the next bit .

Create a Payment Intent



export const getServerSideProps = async () => {
  const stripe = new Stripe("STRIPE_SECRET_KEY_HERE");

  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: "gbp"
  });

  return {
    props: {
      paymentIntent
    }
  };
};


Enter fullscreen mode Exit fullscreen mode

πŸ‘€ If you know more about the customer, for example they are currently logged in, and know their email or shipping address, you can pass this onto the create function too. You can see a full list of arguments over on the Stripe docs.

Next, start the Next development server with npm run dev (or yarn dev) and head to http://localhost:3000/checkout.

πŸŽ‰ Yay! You should see a Payment Intent object!


⚠️ While this is great, every time you visit this page it will create a new Payment Intent, and as we touched on earlier, this isn't recommended.

Retrieve existing Payment Intent

getServerSideProps would be a place to store some kind of cookie that can be checked, and if an existing ID exists, make a call to Stripe to retrieve the Payment Intent.

Install nookies to parse and set our cookies with Next.js context;



npm install nookies # yarn add nookies


Enter fullscreen mode Exit fullscreen mode

Then update pages/checkout.js to import the nookies dependency;



import { parseCookies, setCookie } from "nookies";


Enter fullscreen mode Exit fullscreen mode

Now update getServerSideProps to;



export const getServerSideProps = async ctx => {
  const stripe = new Stripe("STRIPE_SECRET_KEY_HERE");

  let paymentIntent;

  const { paymentIntentId } = await parseCookies(ctx);

  if (paymentIntentId) {
    paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);

    return {
      props: {
        paymentIntent
      }
    };
  }

  paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: "gbp"
  });

  setCookie(ctx, "paymentIntentId", paymentIntent.id);

  return {
    props: {
      paymentIntent
    }
  };
};


Enter fullscreen mode Exit fullscreen mode

PS. Make sure not to override the Stripe Secret with STRIPE_SECRET_KEY_HERE πŸ€ͺ if you're copy/pasting!

Obviously the implementation may differ for your setup, but the idea of this tutorial is to teach the flow and best practices.

Now if you head back to http://localhost:3000/checkout and refresh, you should see the same Payment Intent! πŸŽ‰


You can also see Payments created inside the [Stripe Dashboard]. Depending on how many times you loaded the /checkout page, you should see 2 or more Payments. The most recent should be the one being reused and stored in a cookie.

Payments List

Stripe also shows all activity around payments, which includes the request parameters we sent to paymentIntents.create()

Payment Activity

Client side

Now it's time to capture the users card and process the payment. The Stripe Payment Intents API requires a paymentMethod in order to process the transaction.

We can use the client side Stripe libraries to create a secure paymentMethod which contains our card information which is then passed onto Stripe.

Install dependencies for the frontend:



npm install @stripe/stripe-js @stripe/react-stripe-js # or yarn add @stripe/stripe-js @stripe/react-stripe-js


Enter fullscreen mode Exit fullscreen mode

Configure Stripe on the frontend

Now with these installed, add the following import lines to the top of your pages/checkout.js file:



import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";


Enter fullscreen mode Exit fullscreen mode

We next need to create a Stripe Promise to pass the Elements provider. You'll need your Publishable key from your Stripe Dashboard and above your CheckoutPage function, add the following;



const stripePromise = loadStripe("STRIPE_PUBLISHABLE_KEY");


Enter fullscreen mode Exit fullscreen mode

Let's finally update our CheckoutPage function to wrap our page with Elements and our newly created stripePromise Promise.



const CheckoutPage = props => {
  return (
    <Elements stripe={stripePromise}>
      <pre>{JSON.stringify(props, null, 2)}</pre>
    </Elements>
  );
};


Enter fullscreen mode Exit fullscreen mode

Creating a Checkout Form

Go ahead and create the folder/file components/CheckoutForm.js in the root of your project and add the following;



import React from "react";

const CheckoutForm = ({ paymentIntent }) => {
  const handleSubmit = async e => {
    e.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* TODO */}
    </form>
  );
}

export default CheckoutForm


Enter fullscreen mode Exit fullscreen mode

That's pretty much the basics configured for the CheckoutForm which we next need to import and invoke on our pages/checkout.js page.



// pages/checkout.js

import CheckoutForm from "../components/CheckoutForm";


Enter fullscreen mode Exit fullscreen mode

Also update the CheckoutPage function to pass paymentIntent from props to CheckoutForm.



// pages/checkout.js
const CheckoutPage = ({ paymentIntent }) => (
  <Elements stripe={stripePromise}>
    <CheckoutForm paymentIntent={paymentIntent} />
  </Elements>
);


Enter fullscreen mode Exit fullscreen mode

⚠️ For whatever reason paymentIntent does not exist, you might want to display a message to the user.

Working with the Stripe React hooks

@stripe/react-stripe-js is a new library by Stripe that exposes a few handy hooks and components for us to use. We'll be using;

  • CardElement
  • useStripe
  • useElements

Inside the CheckoutForm function, we'll invoke both of the Stripe hooks so we have a reference to stripe and elements for use inside our handleSubmit function next.



const CheckoutForm = ({ paymentIntent }) => {
  const stripe = useStripe();
  const elements = useElements();

  // ... rest of file
}


Enter fullscreen mode Exit fullscreen mode

Confirm Stripe Intent with React hooks

A method on stripe that we will need to use is confirmCardPayment, which accepts 3 arguments; client_secret, and (optional: data and options).

We already have the client_secret passed down inside paymentIntent through pages/index.js getServerSideProps, and then onto CheckoutForm via a prop.

Let's update the handleSubmit function to send a request to Stripe to confirm the Payment Intent.



const handleSubmit = async e => {
  e.preventDefault(); // Stops the page from reloading!

  try {
    const {
      error,
      paymentIntent: { status }
    } = await stripe.confirmCardPayment(paymentIntent.client_secret);

    if (error) throw new Error(error.message);

    if (status === "succeeded") {
      alert('Payment made!')
    }
  } catch (err) {
    alert(err.message);
  }
};


Enter fullscreen mode Exit fullscreen mode

Since our Payment Intent was created on the server before the page loaded, and in this example we have no user context or stored payment method, we need to create one on the client and send it in the second argument to confirmCardPayment.

This is where the CardElement component comes in. Stripe provides a fully secure and baked credit card input with expiry, CVV and zip/post code.

Let's first add a <CardElement /> and <button /> component to submit the form. Let's also disable the button if the stripe Promise has not resolved yet.



return (
  <form onSubmit={handleSubmit}>
    <CardElement />

    <button type="submit" disabled={!stripe}>
      Pay now
    </button>
  </form>
);


Enter fullscreen mode Exit fullscreen mode

Now if you refresh your checkout page at http://localhost:3000/checkout you should see something a little like this:

Checkout Form with Card input

Now when we click Pay now nothing will happen on the Stripe side because we haven't attached any payment method data to our Payment Intent.

Now let's update handleSubmit to do just that!

We can use elements.getElement and pass in our CardElement import to reference the card input on our page.



const {
  error,
  paymentIntent: { status }
} = await stripe.confirmCardPayment(paymentIntent.client_secret, {
  payment_method: {
    card: elements.getElement(CardElement)
  }
});


Enter fullscreen mode Exit fullscreen mode

Congratulations! We now have a fully functional payment form! πŸŽ‰πŸŽ‰πŸŽ‰

Go give it a try with a test Stripe card. 4242 4242 4242 4242 will get you through with no SCA challenge.

Stripe successful payment


Now we're not done just yet... You'll notice if you refresh the page and try to make a payment it will fail. This is because we reusing the same paymentIntentId we stored in cookies which is now confirmed.

Tidying it up

We've a few things to do;

  • Destroy the paymentIntentId cookie on successful payments
  • Display a success message instead of a payment form
  • Display an error is present

Destroying the paymentIntentId cookie on on successful payments

Inside components/CheckoutForm.js, import destroyCookie from nookes.



// ...
import { destroyCookie } from "nookies";


Enter fullscreen mode Exit fullscreen mode

Now inside our handleSubmit function we check if the status is succeeded. It would be here we would need to call destroyCookie.



// ..
if (status === "succeeded") {
  destroyCookie(null, "paymentIntentId");
}


Enter fullscreen mode Exit fullscreen mode

Implement success/error messages

Now let's import useState from react and invoke the hook for 2 different types of state;

  • checkoutError
  • checkoutSuccess


const CheckoutForm = ({ paymentIntent }) => {
// ...

const [checkoutError, setCheckoutError] = useState();
const [checkoutSuccess, setCheckoutSuccess] = useState();


Enter fullscreen mode Exit fullscreen mode

Now inside handleSubmit, add setCheckoutSuccess while passing true on a successful payment and setCheckoutError(err.message) inside the catch block.



try {
  // ...

  if (status === "succeeded") {
    setCheckoutSuccess(true);
    destroyCookie(null, "paymentIntentId");
  }
} catch (err) {
  alert(err.message);
  setCheckoutError(err.message);
}


Enter fullscreen mode Exit fullscreen mode

Then before we render the form inside return, return a successful paragraph if checkoutSuccess is truthy.



if (checkoutSuccess) return <p>Payment successful!</p>;


Enter fullscreen mode Exit fullscreen mode

Finally, somewhere inside the <form> add the following;



{checkoutError && <span style={{ color: "red" }}>{checkoutError}</span>}


Enter fullscreen mode Exit fullscreen mode

You did it!

Successful payment

If you check the Stripe Dashboard you will also see the successful Payment Intent!

Stripe payment success

Stripe payment success logs


Final pages/checkout.js



import React from "react";
import Stripe from "stripe";
import { parseCookies, setCookie } from "nookies";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "../components/CheckoutForm";

const stripePromise = loadStripe("STRIPE_PUBLISHABLE_KEY");

export const getServerSideProps = async ctx => {
  const stripe = new Stripe("STRIPE_SECRET_KEY");

  let paymentIntent;

  const { paymentIntentId } = await parseCookies(ctx);

  if (paymentIntentId) {
    paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);

    return {
      props: {
        paymentIntent
      }
    };
  }

  paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: "gbp"
  });

  setCookie(ctx, "paymentIntentId", paymentIntent.id);

  return {
    props: {
      paymentIntent
    }
  };
};

const CheckoutPage = ({ paymentIntent }) => (
  <Elements stripe={stripePromise}>
    <CheckoutForm paymentIntent={paymentIntent} />
  </Elements>
);

export default CheckoutPage;


Enter fullscreen mode Exit fullscreen mode

Final CheckoutForm.js



import React, { useState } from "react";
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { destroyCookie } from "nookies";

const CheckoutForm = ({ paymentIntent }) => {
  const stripe = useStripe();
  const elements = useElements();
  const [checkoutError, setCheckoutError] = useState();
  const [checkoutSuccess, setCheckoutSuccess] = useState();

  const handleSubmit = async e => {
    e.preventDefault();

    try {
      const {
        error,
        paymentIntent: { status }
      } = await stripe.confirmCardPayment(paymentIntent.client_secret, {
        payment_method: {
          card: elements.getElement(CardElement)
        }
      });

      if (error) throw new Error(error.message);

      if (status === "succeeded") {
        setCheckoutSuccess(true);
        destroyCookie(null, "paymentIntentId");
      }
    } catch (err) {
      alert(err.message);
      setCheckoutError(err.message);
    }
  };

  if (checkoutSuccess) return <p>Payment successful!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <CardElement />

      <button type="submit" disabled={!stripe}>
        Pay now
      </button>

      {checkoutError && <span style={{ color: "red" }}>{checkoutError}</span>}
    </form>
  );
};

export default CheckoutForm;


Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
notrab profile image
Jamie Barton

Another important factor to remember is the Idempotencey Key you can send with the payment intent. This typically would be an ID for the cart/checkout you want to pay for. This means the same cart/checkout can't be paid for twice with Stripe.

Collapse
 
jai_type profile image
Jai Sandhu

This was amazing to read, thank you so much! What would be the best way to update a paymentId? In the example it's hard-coded, I see from the stripe documentation it is:

const paymentIntent = await stripe.paymentIntents.update(
'pi_1Iqi23IWWEwahlPoqvpAjRHr',
{metadata: {order_id: '6735'}}
);

Could I grab stripe using the useStripe hook and update the payment intent using it's id?

Collapse
 
tesshsu profile image
tess hsu

Hi Jamie,
I had install the code package from github, and replace my strip security and public key in test mode, it's working to send payment ( incomplete ) in strip dashboard

But once I click button "Pay now"
show error : Cannot read property 'status' of undefined

I had looking for this error in strip or doc but nothing to find out
Do you have clue ? I do like your tutorial in localhost:3000/checkout

Collapse
 
tesshsu profile image
tess hsu

Sorry Jamie
problem resolved, I missing zip code which I do not fill in, sorry being bother

Collapse
 
wyjbuss profile image
Wyatt Bussell

Is there a preferred way of keeping track of inventory and showing what items were purchased? I also need information such as location to ship to and email of customer. I saw Itempotencey ID in the comments. Im using a mongo database as well. Would the best way to do this be creating a customer in stripe before they pay?