-
Notifications
You must be signed in to change notification settings - Fork 932
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
Merged
Merged
Changes from 37 commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
fa53b9b
add billing guide and reference page
panteliselef 1b0b52a
fix build
panteliselef 0ca1396
format
panteliselef fc60f8c
add payment element
panteliselef aa288df
add payment method guide
panteliselef ae95ca1
fix incorrect usage
panteliselef 4acef19
example with existing methods
panteliselef ce5d418
include control components
panteliselef a357c20
add custom flow to overview
panteliselef d28d1bd
Update docs/hooks/use-checkout.mdx
panteliselef f150280
Merge branch 'refs/heads/main' into elef/billing-apis
panteliselef e436fac
add usePlans
panteliselef ff92e0e
Merge branch 'main' into elef/billing-apis
panteliselef e54e936
formatting
panteliselef aeced27
add useSubscription
panteliselef c80172b
Merge branch 'main' into elef/billing-apis
panteliselef 22af4b4
add experimental callout
panteliselef bf80eab
change callouts of custom flows
panteliselef 1d27701
fix
panteliselef f2f0bdb
remove invalid example
panteliselef 47e2294
docs review
SarahSoutoul 4eb9c22
Merge branch 'main' into elef/billing-apis
panteliselef ac51109
fix paths
panteliselef 0777d63
address comment about return object
panteliselef 2ed5291
improve useCheckout snippets
panteliselef 9e0c688
format
panteliselef d242237
improve ref types
panteliselef 0bdaf66
docs review pt 1
alexisintech 29dbaba
fix broken hash
alexisintech c800db0
small updates
alexisintech 3184963
address review comments
panteliselef 6505dd4
Merge branch 'main' into elef/billing-apis
panteliselef 10a43ac
docs review pt2
alexisintech 36edb29
add custom flow callout to custom flow guides
alexisintech 4a6e665
fix links
alexisintech 2072c32
address infinite rendering
panteliselef c2c4a62
replace fallback
panteliselef d4d854c
update custom flow
alexisintech 92116f8
replace statuses
panteliselef 2da7f14
update for prop usage
panteliselef 7e4bfca
resolve todos
panteliselef ac3e452
Merge branch 'main' into elef/billing-apis
panteliselef File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| 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 />`**: Sets up the `<PaymentElementProvider />`, which specifies that the payment actions within its children are `for` the `user`. | ||
| - **`<AddPaymentMethodForm />`**: Renders the payment form and handles the submission logic. 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. | ||
|
|
||
| <CodeBlockTabs options={["<UserBillingPage />", "<AddPaymentMethodForm />"]}> | ||
| ```tsx {{ filename: 'app/user/billing/page.tsx' }} | ||
| import { ClerkLoaded } from '@clerk/nextjs' | ||
| import { PaymentElementProvider } from '@clerk/nextjs/experimental' | ||
| import { AddPaymentMethodForm } from './AddPaymentMethodForm' | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <div> | ||
| <h1>Billing Settings</h1> | ||
|
|
||
| <ClerkLoaded> | ||
| <PaymentElementProvider for="user"> | ||
| <AddPaymentMethodForm /> | ||
| </PaymentElementProvider> | ||
| </ClerkLoaded> | ||
| </div> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ```tsx {{ filename: 'app/user/billing/AddPaymentMethodForm.tsx' }} | ||
| 'use client' | ||
| import { useUser } from '@clerk/nextjs' | ||
| import { usePaymentElement, PaymentElement } from '@clerk/nextjs/experimental' | ||
| 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(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> | ||
| <PaymentElement /> | ||
| <button type="submit" disabled={!isFormReady || isSubmitting}> | ||
| {isSubmitting ? 'Saving...' : 'Save Card'} | ||
| </button> | ||
| {error && <p style={{ color: 'red' }}>{error}</p>} | ||
| </form> | ||
| ) | ||
| } | ||
| ``` | ||
| </CodeBlockTabs> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <Properties> | ||
| - `for?` | ||
| - `'organization'` | ||
|
|
||
| Specifies if the checkout is for an organization. If omitted, the checkout defaults to the current user. | ||
|
|
||
| --- | ||
|
|
||
| - `planId` | ||
| - `string` | ||
|
|
||
| The ID of the subscription plan to check out (e.g. `cplan_xxx`). | ||
|
|
||
| --- | ||
|
|
||
| - `planPeriod` | ||
| - `'month' | 'annual'` | ||
|
|
||
| The billing period for the plan. | ||
| </Properties> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| --- | ||
| title: Build a custom flow for adding a new payment method | ||
| description: Learn how to use the Clerk API to build a custom flow for adding a new payment method to a user's account. | ||
| --- | ||
|
|
||
| <Include src="_partials/custom-flows-callout" /> | ||
|
|
||
| <Include src="_partials/billing/api-experimental-guide" /> | ||
|
|
||
| This guide will walk you through how to build a custom user interface that allows users to **add a new payment method to their account**. 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. | ||
|
|
||
| For the custom flow that allows users to add a new payment method **during checkout**, see the [dedicated guide](/docs/custom-flows/checkout-new-payment-method). | ||
|
|
||
| <Steps> | ||
| ### Enable billing features | ||
|
|
||
| To use billing features, you first need to ensure they are enabled for your application. 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 [`<PaymentElement />`](/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> | ||
| <Include src="_partials/billing/add-new-payment-method" /> | ||
| </Tab> | ||
| </Tabs> | ||
| </Steps> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| --- | ||
| 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 that allows users to checkout with an existing payment method. | ||
| --- | ||
|
|
||
| <Include src="_partials/custom-flows-callout" /> | ||
|
|
||
| <Include src="_partials/billing/api-experimental-guide" /> | ||
|
|
||
| This guide will walk you through how to build a custom user interface for a checkout flow that allows users to checkout **with an existing payment method**. For the custom flow that allows users to **add a new payment method** during checkout, see the [dedicated guide](/docs/custom-flows/checkout-new-payment-method). | ||
|
|
||
| <Steps> | ||
| ## Enable billing features | ||
|
|
||
| To use billing features, you first need to ensure they are enabled for your application. 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: | ||
| - [Copy from the Clerk Dashboard](https://dashboard.clerk.com/last-active?path=billing/plans?tab=user). | ||
| - Use the [Clerk Backend API](TODO). | ||
| - Use the new [`usePlans()`](/docs/hooks/use-plans) 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/checkout/page.tsx' }} | ||
| 'use client' | ||
| import * as React from 'react' | ||
| import { SignedIn, ClerkLoaded } from '@clerk/nextjs' | ||
| import { CheckoutProvider, useCheckout, usePaymentMethods } from '@clerk/nextjs/experimental' | ||
| import { useMemo, useState } from 'react' | ||
|
|
||
| export default function CheckoutPage() { | ||
| return ( | ||
| <CheckoutProvider planId="cplan_xxx" 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, finalize, error } = checkout | ||
|
|
||
| const [isProcessing, setIsProcessing] = useState(false) | ||
| const [paymentMethodId, setPaymentMethodId] = useState<string | null>(null) | ||
|
|
||
| const defaultMethod = useMemo(() => data?.find((method) => method.isDefault), [data]) | ||
|
|
||
| const submitSelectedMethod = async () => { | ||
| const paymentSourceId = paymentMethodId || defaultMethod?.id | ||
| if (isProcessing || !paymentSourceId) return | ||
| setIsProcessing(true) | ||
|
|
||
| try { | ||
| // Confirm checkout with payment method | ||
| await confirm({ paymentSourceId }) | ||
| // Complete checkout and redirect | ||
| finalize({ 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) { | ||
| setPaymentMethodId(method.id) | ||
| } | ||
| }} | ||
| > | ||
| {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 | ||
|
|
||
| if (!plan) { | ||
| return null | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <h2>Order Summary</h2> | ||
| <span>{plan.name}</span> | ||
| <span> | ||
| {totals.totalDueNow.currencySymbol} {totals.totalDueNow.amountFormatted} | ||
| </span> | ||
| </div> | ||
| ) | ||
| } | ||
| ``` | ||
| </Tab> | ||
| </Tabs> | ||
| </Steps> | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.