Checkout
A one-page checkout with contact, delivery and payment beside a live order summary. Handles changed totals, sold-out items and unknown outcomes.
Checkout puts contact, delivery and payment on one page, with the order summary beside it on wide screens and folded above it on phones. Each step opens in place and turns into a short summary row when it’s done, so a finished checkout reads like a receipt: “Contact [email protected]”, “Ship to 12 Mission St, San Francisco, CA 94110”, “Method Standard · Arrives Oct 26 – 28 · Free”. Change on any row reopens that step and moves focus to it. Signed-in customers skip contact and start at delivery with their default saved address.
Pass the cart and three callbacks: onGetRates(address), onEstimateTax(address) and onPlaceOrder(draft, payment). Rates and taxes load as soon as the address is complete, with a short pause while someone is typing, and the previous request is aborted through its signal when the address changes. The cheapest rate is picked for you, and the summary shows “Calculating…” rather than a stale number while new prices load.
onPlaceOrder gets a draft with the email, lines, addresses, chosen rate and the exact total the shopper saw, plus the payment request with a fresh idempotency key. Return succeeded with the order, declined with a code, requires_action for 3-D Secure, or changed when the server’s prices differ. A changed result never charges: the summary updates, a notice lists each change in plain words (“Ring vase is now $74, was $68.”) with the old total struck through, and the shopper presses Pay again at the new amount.
The page also handles the slow and strange cases. Pay can’t run twice, and the Change buttons lock while an order is placing. If onPlaceOrder doesn’t answer within 20 seconds, the page says it’s checking the status and polls onCheckStatus with the same idempotency key instead of offering to pay again. An item that sells out mid-checkout is taken out of the total and flagged until the shopper removes it or continues without it, and pressing Pay with gaps lists every problem in one linked summary.
HextaUI Pro holen
Eine einmalige Zahlung schaltet jeden Block frei, auch künftige. Preise ansehen.
Die Pro-Registry zu components.json hinzufügen
components.json Token hinzufügen
Erstelle auf deiner Kontoseite einen Token und trage ihn in
.env.localalsHEXTAUI_PRO_TOKENein.Den Block hinzufügen
pnpm dlx shadcn@latest add @hextaui-pro/checkout
Stripe PaymentIntents with server actions
Pass Stripe’s PaymentElement as paymentElement so card details go straight to Stripe and never touch your server. onPlaceOrder calls elements.submit(), then a server action reprices the cart, returns a changed result if the total moved, or creates a PaymentIntent with the attempt’s idempotency key. confirmPayment handles 3-D Secure, and onCheckStatus asks the server about attempts that never answered.
Shopify checkout handoff
On Shopify, orders and payments go through Shopify’s hosted checkout, so don’t rebuild it: send the email and address to the cart with cartBuyerIdentityUpdate and cartDeliveryAddressesAdd, then open checkoutUrl. Use this block when you take payment yourself, for example on Stripe, Adyen or Medusa.
Medusa cart completion
Rates come from listCartOptions after the address is saved, and taxes from the cart’s tax_total. onPlaceOrder adds the shipping method, compares Medusa’s total with the one the shopper saw and returns a changed result with the line, shipping and tax differences, then starts a payment session and completes the cart.
Aufbau
Die Teile, die du zusammensetzt, von außen nach innen.
| Part | Beschreibung |
|---|---|
Checkout | The full page: header, steps, express wallets, the three sections, payment and the order summary. |
useCheckout | The state behind the page, for building your own layout: steps, address, rates, taxes, totals and order placement. |
StepIndicator | Contact, Delivery and Payment as a breadcrumb. Finished steps get a check and reopen on click. |
SummaryList, SummaryRow | The receipt rows a finished step collapses into, each with a Change action. |
SoldOutNotice | Lists sold-out lines with Remove and Continue without it. |
ChangedNotice | Explains a changed order and shows the new total next to the old one. |
ErrorSummary | One alert that links to every problem stopping payment. |
UnknownNotice, SlowBanner | Status while the outcome is unknown, and a note when the network is slow. |
describeChange, CheckoutChangedError | Plain-language copy for a change, and the error Checkout raises for a changed result. |
| Prop | Typ | Standard |
|---|---|---|
cartThe cart being bought. Lines with soldOut: true are left out of the total and flagged. | Cart | – |
onGetRatesLoads delivery options for a complete address. Throwing shows the message with a retry. | (address: Address, context: { signal: AbortSignal }) => Promise<ShippingRate[]> | – |
onEstimateTaxEstimates tax for the address. Leave it out when prices include tax. | (address: Address, context: { signal: AbortSignal }) => Promise<Money | { amount, included?, basis? } | null> | – |
onPlaceOrderPlaces the order. Return succeeded, declined, requires_action or changed. Throwing shows the message at the payment step. | (draft: CheckoutDraft, payment: CheckoutPaymentRequest) => Promise<PlaceOrderResult<TOrder>> | – |
onCheckStatusAsked every pollInterval once onPlaceOrder has been silent for timeout. The attempt carries the idempotency key and how many checks have run. | (attempt: CheckoutAttempt) => Promise<PlaceOrderResult<TOrder> | { status: "pending" }> | – |
onCompleteCalled after the success check has held. Navigate to your confirmation page here. | (order: TOrder | null) => void | – |
onExpressRuns a wallet payment. Wallet buttons only show when this and expressMethods are set. | (method: ExpressMethod, context: { cart, total, idempotencyKey }) => Promise<PlaceOrderResult<TOrder>> | – |
onRemoveLineRemoves a sold-out line. Without it the notice only offers Continue without it. | (lineId: string) => Promise<Cart | void> | Cart | void | – |
onApplyCode, onRemoveCodeDiscount code handlers for the order summary. Return the updated cart. | (code: string) => Promise<Cart | void> | Cart | void | – |
customerThe signed-in customer. Skips contact and offers saved addresses and cards. | { email: string; name?: string; addresses?: SavedAddress[]; cards?: SavedCard[] } | null | – |
onSignIn, onSignOutShows “Have an account? Sign in” for guests and Sign out on the contact row. | () => void | – |
defaultEmailPrefills the email for guests. | string | "" |
defaultAddressPrefills the shipping address. | Partial<Address> | – |
defaultCountryCountry of the empty address forms. | string | "US" |
expressMethodsWallet buttons above the steps. | ("apple_pay" | "google_pay" | "shop_pay" | "paypal")[] | – |
paymentMethodsMethods in the payment step. | ("card" | "paypal" | "bnpl" | "apple_pay" | "google_pay")[] | ["card", "paypal", "bnpl"] |
paymentElementYour processor’s hosted fields, such as Stripe’s PaymentElement, shown instead of the built-in methods. Pay then sends a request with method: "element" and no card data. | ReactNode | – |
paymentOptions passed to the Payment block, including renderChallenge for your 3-D Secure step. | { bnpl?, renderChallenge?, today?, successDelay?, labels? } | – |
addressFormOptions passed to both address forms, including address autocomplete. | { countries?, phone?, onSearchAddress?, onResolveAddress?, onLookupPostalCode? } | – |
storeNameThe name in the header. | ReactNode | "Halden" |
storeHrefMakes the store name a link back to the shop. | string | – |
onStoreClickCalled on a plain click of the store name instead of following storeHref, for client-side routers. Modified clicks still open the link. | () => void | – |
footerPolicy links under the payment step. | ReactNode | – |
slowAfterMilliseconds before the slow-network note shows for rates or the order. | number | 4000 |
timeoutMilliseconds of silence from onPlaceOrder before polling onCheckStatus. | number | 20000 |
pollIntervalMilliseconds between status checks. | number | 3000 |
addressDelayPause after typing before rates and taxes load. Saved addresses load at once. | number | 450 |
localeFormats prices and dates. | Intl.LocalesArgument | "en-US" |
labelsOverrides any text on the page. | Partial<CheckoutLabels> | – |
useCheckout
useCheckout(options) takes the same data and callbacks as Checkout and returns the state and actions it renders from.
| Prop | Typ | Standard |
|---|---|---|
section, completedThe open step and which steps are done. | "contact" | "delivery" | "payment", { contact: boolean; delivery: boolean } | – |
open, advanceReopen a step, or mark one done and move to the next. open returns false while an order is placing. | (section) => boolean, (from) => section | – |
email, setEmail, marketing, setMarketingThe contact step. | string, boolean and setters | – |
addressDraft, setAddressDraft, savedAddressId, selectSavedAddress, shippingAddressThe delivery address. shippingAddress is null until it’s complete. | Address and setters | – |
rates, rate, selectRate, retryRatesRates for the current address and the chosen one. | RatesState, ShippingRate | undefined, functions | – |
totals, total, shipping, summaryTaxEverything the order summary shows, after overrides from a changed result. | SummaryTotals, Money, … | – |
cart, summaryCart, flaggedLines, removeLine, continueWithoutThe cart, the cart without sold-out lines, and the sold-out lines still waiting for a decision. | Cart, Cart, CartLine[], functions | – |
placement, busy, slowOrder phase (idle, placing, unknown, changed, succeeded), whether editing is locked, and what is slow. | PlacementState, boolean, "rates" | "order" | null | – |
placeOrder, placeExpressPlace the order once. A second call while one is running returns the same promise. A changed result rejects with CheckoutChangedError. | (request) => Promise<PaymentResult> | – |
draftThe draft onPlaceOrder will get, or null while something is missing. | () => CheckoutDraft | null | – |
finishCalls onComplete with the placed order. | () => void | – |
| Prop | Typ | Standard |
|---|---|---|
sectionThe current step, marked aria-current="step". | CheckoutSection | – |
completedSteps that get a check. | { contact: boolean; delivery: boolean } | – |
canOpenWhether a step is a button. | (section) => boolean | – |
onOpenCalled when a step is clicked. | (section) => void | – |
labels, labelStep names and the nav label. | Record<CheckoutSection, string>, string | – |
| Prop | Typ | Standard |
|---|---|---|
labelThe row label, such as Contact or Method. | string | – |
childrenThe value. | ReactNode | – |
action, actionLabel, onActionThe Change button, its accessible name and handler. | string, string, () => void | – |
disabledLocks the action, for example while an order is placing. | boolean | – |
| Prop | Typ | Standard |
|---|---|---|
linesSold-out lines. The notice folds away when empty. | CartLine[] | – |
removingThe line being removed. | string | null | – |
errorA failed removal, shown under its line. | { lineId: string; message: string } | null | – |
canRemoveShows Remove next to Continue without it. | boolean | – |
onRemove, onContinueThe two choices. | (lineId: string) => void | – |
| Prop | Typ | Standard |
|---|---|---|
changesEach change, written out with describeChange. | CheckoutChange[] | – |
previousTotalThe total the shopper tried to pay, struck through. | Money | null | – |
totalThe new total. | Money | – |
onDismissAdds a Got it button. | () => void | – |
| Prop | Typ | Standard |
|---|---|---|
titleDie Überschrift. | string | "There is a problem" |
entriesOne link per problem. onSelect opens and focuses the field. | { id: string; message: string; onSelect: () => void }[] | – |
describeChange
describeChange(change, { locale }) returns a sentence such as “Only 2 Stoneware cup left, so we changed your quantity from 4 to 2.”
| Prop | Typ | Standard |
|---|---|---|
changeOne entry from a changed result. | { type: "price" | "quantity" | "sold_out" | "shipping" | "tax" | "discount"; … } | – |
| Taste | Aktion |
|---|---|
| Tab | Move through the open step, then the summary rows and their Change buttons. |
| Enter | Continue to delivery from the email field. |
| ↑↓ | Move between saved addresses, delivery options and payment methods. |
| Space | Toggle the marketing and billing checkboxes. |
| Enter | Follow a link in the error summary to the field it names. |
- Each step is a section labelled by its heading, and the breadcrumb marks the current one with aria-current="step". When a step opens, finishes or is reopened with Change, focus moves to its heading or first field, so keyboard and screen reader users land where the content changed. Programmatic focus never draws a focus ring; only keyboard focus does.
- Closed steps are inert, so Tab never reaches hidden fields. Change buttons name what they change, such as “Change contact” and “Change ship to address”.
- Pressing Pay with problems shows one alert that lists them as links and takes focus. A changed order and a decline are alerts that take focus, pressing Pay with a sold-out item moves focus to its notice, and loading rates, a removed item and the status check are announced politely.
- Inputs are 16px on touch screens, buttons are 48px tall on touch, steps fold with a short height and fade transition that turns off with reduced motion, and status text never shifts the layout.
Gebaut mit
Die kostenlosen HextaUI-Komponenten, aus denen Checkout besteht. Jede lässt sich einzeln installieren.
Code
7 Dateien, hinzugefügt zu components/blocks/checkout.