Skip to content

Add Billing hooks and custom flows #2385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/_partials/billing/api-experimental-guide.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> [!WARNING]
>
> This guide is using experimental APIs and subject to change while Clerk Billing is under Beta. To mitigate potential disruptions, we recommend [pinning](https://docs.renovatebot.com/dependency-pinning/#what-is-dependency-pinning) your SDK and `clerk-js` package versions.
3 changes: 3 additions & 0 deletions docs/_partials/billing/api-experimental.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> [!WARNING]
>
> This API is experimental and subject to change while Clerk Billing is under Beta. To mitigate potential disruptions, we recommend [pinning](https://docs.renovatebot.com/dependency-pinning/#what-is-dependency-pinning) your SDK and `clerk-js` package versions.
7 changes: 7 additions & 0 deletions docs/billing/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ Clerk billing allows your customers to purchase recurring subscriptions to your

- [Billing for B2B SaaS](/docs/billing/b2b-saas)
- To charge companies or organizations

---

- [Build a simple checkout page](/docs/custom-flows/checkout-new-payment-method)
- To charge users with a new payment method

---
</Cards>
114 changes: 114 additions & 0 deletions docs/custom-flows/add-new-payment-methods.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: Add a new payment method
description: Learn how to use Clerk's components to allow users to add a new payment method to their account.
---

<Include src="_partials/billing/api-experimental-guide" />

This guide will walk you through how to build a flow for adding a new payment method. This is a common feature in a user's billing or account settings page, allowing them to pre-emptively add a payment method for future use.

<Steps>
### Enable billing features

To use billing features, you first need to ensure they are enabled for your application. Please follow the [Billing documentation](/docs/billing/overview) to enable them and set up your plans.

### Add payment method flow

To add a new payment method for a user, you must:

1. Set up the [`<PaymentElementProvider />`](/docs/hooks/use-payment-element) to create a context for the user's payment actions.
1. Render the [`<PaymentElementForm />`](/docs/hooks/use-payment-element) to display the secure payment fields from your provider.
1. Use the [`usePaymentElement()`](/docs/hooks/use-payment-element) hook to submit the form and create a payment token.
1. Use the [`useUser()`](/docs/hooks/use-user) hook to attach the newly created payment method to the user.

<Tabs items={["Next.js"]}>
<Tab>
The following example demonstrates how to create a billing page where a user can add a new payment method. It is split into two components:

- **`<UserBillingPage />`**: This component sets up the `<PaymentElementProvider />`.
- **`<AddPaymentMethodForm />`**: This component renders the payment form and handles the submission logic using the `usePaymentElement` and `useUser` hooks.

<Tabs items={["UserBillingPage", "AddPaymentMethodForm"]}>
<Tab>
This component is responsible for setting up the provider context. It specifies that the payment actions within its children are `for` the `user`.

```tsx {{ filename: 'src/components/UserBillingPage.tsx' }}
import { PaymentElementProvider } from '@clerk/nextjs'
import { AddPaymentMethodForm } from './AddPaymentMethodForm'

export function UserBillingPage() {
return (
<div>
<h1>Billing Settings</h1>
<p>Manage your saved payment methods.</p>

<PaymentElementProvider for="user">
<AddPaymentMethodForm />
</PaymentElementProvider>
</div>
)
}
```
</Tab>

<Tab>
This component contains the form and the logic to handle the submission. It uses `usePaymentElement` to get the `submit` function and `useUser` to get the `user` object. When the form is submitted, it first creates a payment token and then attaches it to the user.

```tsx {{ filename: 'src/components/AddPaymentMethodForm.tsx' }}
import { usePaymentElement, useUser, PaymentElementForm } from '@clerk/nextjs'
import { useState } from 'react'

export function AddPaymentMethodForm() {
const { user } = useUser()
const { submit, isFormReady } = usePaymentElement()
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)

const handleAddPaymentMethod = async (e: React.FormEvent) => {
e.preventDefault()
if (!isFormReady || !user) {
return
}

setError(null)
setIsSubmitting(true)

try {
// 1. Submit the form to the payment provider to get a payment token
const { data, error } = await submit()

// Usually a validation error from stripe that you can ignore.
if (error) {
setIsSubmitting(false)
return
}

// 2. Use the token to add the payment source to the user
await user.addPaymentSource(result.data)

// 3. Handle success (e.g., show a confirmation, clear the form)
alert('Payment method added successfully!')
} catch (err: any) {
setError(err.message || 'An unexpected error occurred.')
} finally {
setIsSubmitting(false)
}
}

return (
<form onSubmit={handleAddPaymentMethod}>
<h3>Add a new payment method</h3>
<PaymentElementForm />
<button type="submit" disabled={!isFormReady || isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Card'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
)
}
```
</Tab>
</Tabs>
</Tab>
</Tabs>
</Steps>
172 changes: 172 additions & 0 deletions docs/custom-flows/checkout-existing-payment-method.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
title: Build a custom checkout flow with an existing payment method
description: Learn how to use the Clerk API to build a custom checkout flow using an existing payment method.
---

<Include src="_partials/billing/api-experimental-guide" />

This guide will walk you through how to build a custom checkout flow using an existing payment method.

<Steps>
## Enable billing features

To use billing features, you first need to ensure they are enabled for your application. Please follow the [Billing documentation](/docs/billing/overview) to enable them and setup your plans.

## Checkout flow

To create a checkout session with an existing payment method, you must:

1. Set up the checkout provider with plan details.
1. Initialize the checkout session when the user is ready.
1. Fetch and display the user's existing payment methods.
1. Confirm the payment with the selected payment method.
1. Complete the checkout process and redirect the user.

<Tabs items={["Next.js"]}>
<Tab>
The following example:

1. Uses the [`useCheckout()`](/docs/hooks/use-checkout) hook to initiate and manage the checkout session.
1. Uses the [`usePaymentMethods()`](/docs/hooks/use-payment-methods) hook to fetch the user's existing payment methods.
1. Assumes that you have already have a valid `planId`, which you can acquire in many ways:
1. Copy from the Clerk Dashboard.
1. Use the [Clerk Backend API](TBD).
1. 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.

```tsx {{ filename: 'app/components/CheckoutPage.tsx', mark: [[3, 9], 14, 15, 17, 18, 23, [26, 28], [55, 58], [71, 77], [102, 104]] }}
'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>
)
}
```
</Tab>
</Tabs>
</Steps>
Loading