👋🏽 Hey there! There's video version of this on a recent demo I did: https://youtu.be/ArZO3qtS7EQ?t=3653. There's also a GitHub repo that has all of this code as well.

Introduction

WooCommerce doesn't have the concept of a hosted checkout for your headless, decoupled application. This is an intro on how to set up a checkout page with your headless WooCommerce app with Stripe and GraphQL. It's a basic demo that should provide you some tools to get you started.

I'll be using Gatsby as my headless client, but a lot of the logic should work with other libraries/frameworks too.

On the client side, we'll use the Stripe React library, which will abstract a lot of the Stripe logic for us.

Versions

Prerequisites

  1. Create a free account with Stripe and get your test API public key.

  2. Install the Stripe React library library in your Gatsby project

npm install --save @stripe/react-stripe-js @stripe/stripe-js
  1. Install the free WooCommerce Gateway Stripe plugin in your WordPress site

Step 1: Create checkout page and load Stripe

Create a checkout page, load Stripe and wrap your page in the Elements provider.

// src/pages/checkout.tsx

import React from 'react'
import { PageProps } from 'gatsby'
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'

const stripePromise = loadStripe(
  'pk_test_12345...' // <- Get this from your Stripe account
)

const Checkout: React.FC<PageProps> = () => {
  return (
    <>
      <h1>Checkout</h1>
      <Elements stripe={stripePromise}></Elements>
    </>
  )
}

export default Checkout

As an aside, you could always wrap your entire app in this provider. I'd lean to loading it in the checkout page so that we only load Stripe when we need it 🚀.

This is the setup that Stripe needs to load all of its dependencies. On the next step, we'll create our checkout form to build out the necessary credit card fields that a user will fill out.

Step 2: Create the checkout form

Let's create a basic checkout form so that we can enter our credit card information. For simplicity, we'll hard-code the other information about the buyer such as the name and address.

// src/components/CheckoutForm.tsx

import React from 'react'
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
import { Source } from '@stripe/stripe-js'
import { gql, useMutation } from '@apollo/client'
import { navigate } from 'gatsby'

const CheckoutForm: React.FC = () => {
  // This loads up the Stripe object
  const stripe = useStripe()

  // Used to pass the payment info to the Stripe API
  const elements = useElements()

  // We'll deal with this in a sec
  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault()
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* CardElement will load up the necessary CC fields */}
      <CardElement
        options={{
          hidePostalCode: true, // We'll be sending up the postal ourselves
        }}
      />
      <button disabled={!stripe}>Pay</button>
    </form>
  )
}

export default CheckoutForm

Lots of stuff happening here, but this is the skeleton of our form. We'll load up two elements from Stripe and use the CardElement component to render out the credit card fields.

Step 3: Form submission

When the form is submitted, we will be utilizing the Stripe API to generate a Source object. Essentially, the Source object contains all of the payment information that a user has submitted. It contains an ID, which will serve as a reference we'll be sending to WooCommerce. WooCommerce will then use that ID during the order and payment process logic.

Our responsibility on the client side is simply to capture the credit card information and send it to Stripe. We'll eventually be sending WooCommerce that same Source ID

The meat of our handleStripe function is in the createSource method that's available from the stripe object. We'll either return the Source type or an Error.

async function handleSubmit(event: React.FormEvent) {
  event.preventDefault()
  try {    const source = await handleStripe()  } catch (error) {    console.error(error)  }}

async function handleStripe(): Promise<Source | Error> {
  // Guard against stripe or elements not being available
  if (!stripe || !elements) {
    throw Error(`stripe or elements undefined`)
  }

  // Extract the payment data from our <CardElement/> component
  const cardElements = elements.getElement(CardElement)

  // Guard against an undefined value
  if (!cardElements) {
    throw Error(`cardElements not found`)
  }

  // Create the Source object
  const { source, error: sourceError } = await stripe.createSource(
    cardElements,
    {
      type: `card`,
    }
  )

  // Guard against and error or undefined source
  if (sourceError || !source) {
    throw Error(sourceError?.message || `Unknown error generating source`)
  }

  return source
}

Step 4: Send GraphQL mutation to WooCommerce

Once we've handled the Stripe side of things, the last step to complete the checkout is to send a checkout mutation to WooCommerce. In the mutation arguments, we'll pass in our newly-created source ID and let WooCommerce know that we intend to use Stripe.

import React from 'react'
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
import { Source } from '@stripe/stripe-js'
import { gql, useMutation } from '@apollo/client'import { navigate } from 'gatsby'

const CheckoutForm: React.FC = () => {
  const stripe = useStripe()
  const elements = useElements()

  const [checkout] = useMutation(CHECKOUT)
  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault()
    try {
      const source = await handleStripe()

      await checkout({        variables: {          input: {            clientMutationId: '12345',            paymentMethod: 'stripe', // <-- Hey WooCommerce, we'll be using Stripe            shippingMethod: 'Flat rate',            billing: { // <-- Hard-coding this for simplicity              firstName: 'George',              lastName: 'Costanza',              address1: `129 West 81st Street, Apartment 5A`,              city: `New York`,              state: `NY`,              postcode: `12345`,              email: `george@vandelayindustries.com`,            },            metaData: [              {                key: `_stripe_source_id`,                value: source.id,              },            ],          },        },      })    } catch (error) {
      console.error(error)
    }
  }

  async function handleStripe(): Promise<Source | Error> {
    ...
  }

  return (
    ...
  )
}

const CHECKOUT = gql`  mutation Checkout($input: CheckoutInput!) {    checkout(input: $input) {      order {        databaseId      }    }  }`
export default CheckoutForm

A few important things to note here. I'm using Apollo as my http client. You can choose whatever fits best for your project. The important point is that we're sending a GraphQL mutation to WooCommerce.

Another important assumption here is that I'm sending along the WooCommerce cart session in my headers as part of the request. That is beyond scope of what we're doing here, but please read the details of this pull request to know what's happening behind the scenes.

The two critical pieces of data relevant to get Stripe working is the paymentMethod and _stripe_source_id values.

WooGraphQL will use the paymentMethod value to dyanimically run the payment process method for the Stripe Payment Gateway add-on in WordPress.

The _stripe_source_id value will get saved as order meta by WooGraphQL. It will then be retrieved by WooCommerce Gateway Stripe during the payment process logic down the line.

Step 5: Load the CheckoutForm component in your checkout page

The last step is to load your new form in the checkout page. We'll simply ensure that it's wrapped by the <Elements/> provider.

// src/pages/checkout.tsx

import React from 'react'
import { PageProps } from 'gatsby'
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import CheckoutForm from '../../components/CheckoutForm'
const stripePromise = loadStripe('pk_test_12345...')

const Checkout: React.FC<PageProps> = () => {
  return (
    <>
      <h1>Checkout</h1>
      <Elements stripe={stripePromise}>
        <CheckoutForm />      </Elements>
    </>
  )
}

export default Checkout

Step 6: What to do after a successful checkout

For simplicity, we're not doing anything on a successful checkout. However, one option you can do is redirect the user to a thank you page.

In Gatsby, that could look like the following:

import { navigate } from 'gatsby'

...

const [checkout] = useMutation(CHECKOUT, {
  // Send the user to a thank you page after success and send along some state
  onCompleted({ checkout }) {
    navigate('/checkout/order-received', { state: checkout.order })
  },
  // Handle errors
  onError(error) {
    console.error(error)
  }
})

...

Apollo exposes a onCompleted method to us that we can run. In it, our checkout mutation is available. We could then use Gatsby's navigate helper function to programatically route the user to a new page we've created and send along some state (the order).

Conclusion

Our example is really contrived; however, we were able to make the connections between Gatsby, Stripe and WooCommerce. From here, it's a matter of adding in the rest of the UI entities like validation, proper error handling and such.