Skip to main content
Docs

Build a custom checkout flow for a new payment method

Warning

This guide is using experimental APIs and subject to change while Clerk Billing is under Beta. To mitigate potential disruptions, we recommend pinning your SDK and clerk-js package versions.

This guide will walk you through how to build a custom checkout flow with a new payment method.

Enable billing features

To use billing features, you first need to ensure they are enabled for your application. Please follow the Billing documentation to enable them and setup your plans.

Checkout flow

To create a checkout session with a new payment card, you must:

  1. Set up the checkout provider with plan details.
  2. Initialize the checkout session when the user is ready.
  3. Render the payment form for card collection.
  4. Confirm the payment with the collected payment method.
  5. Complete the checkout process and redirect the user.

The following example:

  1. Uses the useCheckout() hook to get to initiate and manage the checkout session.
  2. Uses the usePaymentElement() hook to control the payment element, which is rendered by <PaymentElementForm/>.
  3. Assumes that you have already have a valid planId, which you can acquire in many ways.
    1. Copy from the Clerk Dashboard.
    2. Use the Clerk Backend API.
    3. Use the new usePlans() hook to get the plan details.

This example is written for Next.js App Router but can be adapted for any React-based framework.

app/components/CheckoutPage.tsx
'use client'
import * as React from 'react'
import {
  CheckoutProvider,
  useCheckout,
  PaymentElementProvider,
  PaymentElementForm,
  usePaymentElement,
  SignedIn,
  ClerkLoaded,
} from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function CheckoutPage() {
  return (
    <CheckoutProvider for="user" planId="cplan_xxxx" planPeriod="month">
      <ClerkLoaded>
        <SignedIn>
          <CustomCheckout />
        </SignedIn>
      </ClerkLoaded>
    </CheckoutProvider>
  )
}

function CustomCheckout() {
  const { checkout } = useCheckout()
  const { status } = checkout

  if (status === 'awaiting_initialization') {
    return <CheckoutInitialization />
  }

  return (
    <div className="checkout-container">
      <CheckoutSummary />

      <PaymentElementProvider checkout={checkout}>
        <PaymentSection />
      </PaymentElementProvider>
    </div>
  )
}

function CheckoutInitialization() {
  const { start, status, fetchStatus } = useCheckout()

  if (status === 'awaiting_initialization') {
    return <CheckoutInitialization />
  }

  return (
    <button onClick={start} disabled={fetchStatus === 'fetching'} className="start-checkout-button">
      {fetchStatus === 'fetching' ? 'Initializing...' : 'Start Checkout'}
    </button>
  )
}

function PaymentSection() {
  const { checkout } = useCheckout()
  const { isConfirming, confirm, complete, error } = checkout

  const { isFormReady, submit } = usePaymentElement()
  const [isProcessing, setIsProcessing] = React.useState(false)

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!isFormReady || isProcessing) return
    setIsProcessing(true)

    try {
      // Submit payment form to get payment method
      const { data, error } = await submit()
      // Usually a validation error from stripe that you can ignore
      if (error) {
        return
      }
      // Confirm checkout with payment method
      await confirm(data)
      // Complete checkout and redirect
      await complete({ redirectUrl: '/dashboard' })
    } catch (error) {
      console.error('Payment failed:', error)
    } finally {
      setIsProcessing(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElementForm fallback={<PaymentSkeleton />} />

      {error && <div>{error.message}</div>}

      <button type="submit" disabled={!isFormReady || isProcessing || isConfirming}>
        {isProcessing || isConfirming ? 'Processing...' : 'Complete Purchase'}
      </button>
    </form>
  )
}

function CheckoutSummary() {
  const { checkout } = useCheckout()
  const { plan, totals } = checkout

  return (
    <div>
      <h2>Order Summary</h2>
      <span>{plan?.name}</span>
      <span>\({totals?.totalDueNow.amountFormatted}\)</span>
    </div>
  )
}

Feedback

What did you think of this content?

Last updated on