Command Palette

Search for a command to run...

Set Up Express Checkout

In this guide, you'll learn how the storefront's express checkout row picks the wallet buttons it shows, and how to add a button for a payment provider.

The row sits above the checkout form. Each button pays for the whole order from its wallet's window. The customer picks the address there, the row answers with the shipping and the new total, and the starter places the order once the customer pays. The starter ships the registry empty, so the row renders nothing until you add a payment plugin's button.

How the Row Finds Payment Providers

loads the payment providers enabled in the cart's region, the same list the payment step uses, and passes it to . The row keeps a button only when its registry entry matches one of those providers:

src/modules/checkout/components/express-checkout/index.tsx
1const buttons = expressButtons.flatMap(({ match, Component }) => {
2 const provider = availablePaymentMethods.find((method) => match(method.id))
3 return provider && !unavailable.includes(provider.id)
4 ? [{ providerId: provider.id, Component }]
5 : []
6 })

The registry lists one entry per payment provider, and the starter's list is empty:

src/modules/checkout/components/express-checkout/registry.ts
1/**
2 * Express buttons above the checkout, one entry per payment provider. The row
3 * shows an entry only when `match` accepts a provider enabled in the cart's
4 * region. The starter ships without entries, so the row renders nothing until a
5 * payment plugin's button is added here.
6 */
7export const expressButtons: {
8 match: (providerId: string) => boolean
9 Component: ComponentType<ExpressButtonProps>
10}[] = []

Important: a button can still hide itself after the row renders it. When a wallet is not available to the customer, for example because the wallet's script fails to load, the button calls , and the row drops it. Without any button left, the row renders nothing, not even its title.

What a Button Receives

Every button gets the same props from the row:

src/modules/checkout/components/express-checkout/registry.ts
1export type ExpressButtonProps = {
2 cart: HttpTypes.StoreCart
3 /** The payment provider of the button, as enabled in the cart's region. */
4 providerId: string
5 /** The shipping option the checkout has selected, for the wallet's first quote. */
6 shippingOptionId: string | null
7 /** The wallet is not available to this customer, so the row hides the button. */
8 onUnavailable: () => void
9 onError: (error: ExpressCheckoutError | null) => void
10 /** True while the wallet's payment turns into an order. */
11 onBusy: (busy: boolean) => void
12 /** Places the order once the wallet has paid. A placed order redirects. */
13 placeOrder: () => Promise<void>
14 /**
15 * The wallet's window closed without an order. The cart may now hold the
16 * wallet's address, so the row refreshes the checkout.
17 */
18 onClosed: () => void
19}

A button talks to Medusa through the server actions in . None of them revalidates the cart, because the wallet's window waits for each answer and a revalidation renders the whole checkout again inside the action's response. The row refreshes the page once, through , after the window has closed.

Add a Button for a Payment Provider

To add a button for a payment provider:

  1. Create a component under , taking . Start the payment session with as soon as the wallet needs it. PayPal needs it on the click, Stripe after the customer approves. Answer the wallet's address changes with , and once the customer pays, save the contact with and call .
  2. Add an entry for it to in , with a that accepts the payment provider's id.
  3. Enable the payment provider in the regions where the button should show, under Settings → Regions in Medusa Admin.

The PayPal plugin's example storefront adds its button this way:

src/modules/checkout/components/express-checkout/registry.ts
1import { isPaypal } from "@lib/constants"
2import PayPalExpressButton from "./buttons/paypal"
3
4// ...
5
6export const expressButtons: {
7 match: (providerId: string) => boolean
8 Component: ComponentType<ExpressButtonProps>
9}[] = [
10 { match: (providerId) => Boolean(isPaypal(providerId)), Component: PayPalExpressButton },
11]

The whole button, with the address changes and the check of the approved amount, is described in Integrate PayPal Payment into Storefront. The Stripe plugin's example adds Apple Pay, Google Pay and Link to the row the same way, with an entry that matches . The button is described in Integrate Stripe Payment into Storefront.

Test the Row

The row and its buttons work on with the sandbox credentials of a wallet. A wallet's webhook, which completes the order when the customer closes the tab after paying, needs the backend on a public address. The example projects of the payment plugins, such as the PayPal example, run it through a tunnel with .

References

Edited Oct 27, 2026·Edit this page