Build a custom checkout flow with an existing payment method
This guide will walk you through how to build a custom checkout flow using an existing 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 an existing payment method, you must:
- Set up the checkout provider with plan details.
- Initialize the checkout session when the user is ready.
- Fetch and display the user's existing payment methods.
- Confirm the payment with the selected payment method.
- Complete the checkout process and redirect the user.
The following example:
- Uses the
useCheckout()
hook to initiate and manage the checkout session. - Uses the
usePaymentMethods()
hook to fetch the user's existing payment methods. - Assumes that you have already have a valid
planId
, which you can acquire in many ways:- Copy from the Clerk Dashboard.
- Use the Clerk Backend API.
- 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.
'use client'
import * as React from 'react'
import {
__experimental_CheckoutProvider as CheckoutProvider,
__experimental_useCheckout as useCheckout,
__experimental_usePaymentMethods as usePaymentMethods,
SignedIn,
ClerkLoaded,
} from '@clerk/nextjs'
import { useMemo, useState } from 'react'
export default function CheckoutPage() {
return (
<CheckoutProvider 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 />
<PaymentSection />
</div>
)
}
function CheckoutInitialization() {
const { checkout } = useCheckout()
const { start, status, fetchStatus } = checkout
if (status !== 'awaiting_initialization') {
return null
}
return (
<button onClick={start} disabled={fetchStatus === 'fetching'} className="start-checkout-button">
{fetchStatus === 'fetching' ? 'Initializing...' : 'Start Checkout'}
</button>
)
}
function PaymentSection() {
const { checkout } = useCheckout()
const { data, isLoading } = usePaymentMethods({
for: 'user',
pageSize: 20,
})
const { isConfirming, confirm, complete, error } = checkout
const [isProcessing, setIsProcessing] = useState(false)
const [selectedMethod, setSelectedMethod] = useState<(typeof data)[number] | null>(null)
const defaultMethod = useMemo(() => data?.find((method) => method.isDefault), [data])
const submitSelectedMethod = async () => {
if (isProcessing) return
setIsProcessing(true)
try {
// Confirm checkout with payment method
await confirm({
paymentSourceId: (selectedMethod || defaultMethod)?.id,
})
// Complete checkout and redirect
await complete({ redirectUrl: '/dashboard' })
} catch (error) {
console.error('Payment failed:', error)
} finally {
setIsProcessing(false)
}
}
if (isLoading) {
return <div>Loading...</div>
}
return (
<>
<select
defaultValue={defaultMethod?.id}
onChange={(e) => {
const methodId = e.target.value
const method = data?.find((method) => method.id === methodId)
if (method) {
setSelectedMethod(method)
}
}}
>
{data?.map((method) => (
<option key={method.id}>
**** **** **** {method.last4} {method.cardType}
</option>
))}
</select>
{error && <div>{error.message}</div>}
<button type="button" disabled={isProcessing || isConfirming} onClick={submitSelectedMethod}>
{isProcessing || isConfirming ? 'Processing...' : 'Complete Purchase'}
</button>
</>
)
}
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
Last updated on