Search for a command to run...
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.
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.tsx1const 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.ts1/**2 * Express buttons above the checkout, one entry per payment provider. The row3 * shows an entry only when `match` accepts a provider enabled in the cart's4 * region. The starter ships without entries, so the row renders nothing until a5 * payment plugin's button is added here.6 */7export const expressButtons: {8 match: (providerId: string) => boolean9 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.
Every button gets the same props from the row:
src/modules/checkout/components/express-checkout/registry.ts1export type ExpressButtonProps = {2 cart: HttpTypes.StoreCart3 /** The payment provider of the button, as enabled in the cart's region. */4 providerId: string5 /** The shipping option the checkout has selected, for the wallet's first quote. */6 shippingOptionId: string | null7 /** The wallet is not available to this customer, so the row hides the button. */8 onUnavailable: () => void9 onError: (error: ExpressCheckoutError | null) => void10 /** True while the wallet's payment turns into an order. */11 onBusy: (busy: boolean) => void12 /** 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 the16 * wallet's address, so the row refreshes the checkout.17 */18 onClosed: () => void19}
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.
| Action | What it does |
|---|---|
| Puts the address the wallet shares before payment on the cart, checks that the cart's region ships to its country, and picks the shipping. It keeps the option the checkout selected when it still fits, otherwise the first in the order the checkout offers them, skipping pickup and options priced by a choice the customer has to make. Returns the new total with the option's name and price, for a wallet that lists the shipping it charges | |
| Saves the email, the phone and the full address the wallet shares after payment, sets the shipping again, and refuses a total above , the amount the customer approved in the wallet | |
| Starts the payment session the wallet pays and returns its data. With , it keeps a pending session that already holds the same value, for example the same wallet order | |
| Renders the checkout again with the cart as it is now |
To add a button for a payment provider:
The PayPal plugin's example storefront adds its button this way:
src/modules/checkout/components/express-checkout/registry.ts1import { isPaypal } from "@lib/constants"2import PayPalExpressButton from "./buttons/paypal"34// ...56export const expressButtons: {7 match: (providerId: string) => boolean8 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.
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 .