Checkout — WordPress.com Purchase Flow
Customer-facing checkout UI. Related billing areas:
client/me/purchases/ (Classic), client/dashboard/me/billing-purchases/ (Dashboard).
Project Knowledge
Directory Structure
client/my-sites/checkout/
├── src/
│ ├── components/
│ │ ├── checkout-main.tsx # Top-level orchestrator
│ │ ├── item-variation-picker/ # Billing cycle selector (name misleads — not "variants")
│ │ └── wp-contact-form/ # Contact form (varies by product type — see Checkout Steps)
│ ├── payment-methods/ # One UI component per processor
│ ├── hooks/use-create-payment-methods/ # Generates PaymentMethod[] from cart + server config
│ └── lib/
│ ├── wpcom-store.ts # @wordpress/data store (not Redux — checkout-specific state)
│ ├── leave-checkout.ts # navigate() trap — see pitfall #6
│ └── *-processor.ts # One per payment method (13 files)
├── get-thank-you-page-url/ # 800+ lines, exhaustive tests — see pitfall #5
└── checkout-thank-you/ # Post-purchase pages (8+ variants)
Checkout Steps
Three hard-coded steps (not extensible without changes here):
- Review Order
- Contact Details — Form varies by cart contents:
- No domains → billing address only
- Domain registration → full ICANN contact details
- Google Workspace → company name + billing address
- Contact type determined by
getContactDetailsType(responseCart)fromwpcom-checkout
- Payment Method
The sidebar/summary view is NOT a step — visibility is manually managed via
isSummaryVisible state to prevent it from blocking form submission.
Checkout State
- Redux — global state (site, user, notices)
@wordpress/datastore (wpcom-store.ts) — checkout-specific: contact details, VAT, domain validation results, form touched fieldsuseFormStatus()fromcomposite-checkout— LOADING, READY, SUBMITTINGuseTransactionStatus()— NOT_STARTED, PENDING, COMPLETE, REDIRECTING, ERROR
Payment Processing
Processors must handle four response paths: immediate success, redirect (PayPal/WeChat), 3DS challenge (Stripe), and polling (PIX). Not all paths apply to all processors.
Package Boundaries
| Package | Role | Key Rule |
|---|---|---|
composite-checkout | Generic multi-step checkout framework | NO WP.com logic here |
wpcom-checkout | WP.com-specific checkout (line items, tax, payment methods) | WP.com logic goes here |
shopping-cart | Cart state via useShoppingCart() | Independent of checkout |
calypso-stripe | Stripe.js wrapper | Stripe-specific integration |
api-core | Fetchers, mutators, types for all API calls | Foundation layer |
api-queries | TanStack Query wrappers around api-core | Dashboard consumes these |
Architectural Decisions
-
Thank-you URL generated before transaction — For redirect payment methods (PayPal, Bancontact), the thank-you URL is generated and passed to the processor BEFORE the transaction starts. Uses
:receiptIdplaceholder, replaced by/me/transactionsendpoint on return. -
Contact validation is two-step — Form validation (required fields, format) then async domain registration validation (WPCOM backend). Both must pass.
Adding a Payment Method
Seven touchpoints required (follow an existing implementation like Razorpay):
- Payment method component —
src/payment-methods/{name}.tsx, exportcreate{Name}PaymentMethod()returning aPaymentMethodobject ({ id, paymentProcessorId, label, activeContent, submitButton }) - Processor function —
src/lib/{name}-processor.ts, signature:async (submitData, options, translate) => PaymentProcessorResponse - Register processor — Add to processor map in
src/components/checkout-main.tsx - Create hook —
src/hooks/use-create-payment-methods/use-create-{name}.ts, optionally gate withisEnabled('checkout/{name}')for gradual rollout - Register hook — Call in
use-create-payment-methods/index.tsx, add result topaymentMethodsarray - Slug mapping — Add bidirectional mapping in
packages/wpcom-checkout/src/translate-payment-method-names.ts(e.g.,'pix'↔'WPCOM_Billing_Ebanx_Redirect_Brazil_Pix') - Feature flag — (Optional) Add to
config/{environment}.jsonfor gradual rollout; remove once fully enabled
Steps 6-7 are the ones agents miss — without slug mapping the method never appears.
Common Pitfalls
-
Checkout links MUST include
redirect_toandcancel_toparams — Missing these causes broken back/cancel navigation. -
Cart key matters — Wrong cart key = wrong cart.
'no-site'vs site ID vsundefinedhave different behaviors. Seepackages/shopping-cart/README.md. -
Checkout URL format — Products use slugs, domain meta uses colon separator. Example:
/checkout/example.com/personal,domain_reg:example.com. Don't construct URLs manually — use existing helpers. -
Payment method filtering — Not all methods are available everywhere.
filterAppropriatePaymentMethods()inpackages/wpcom-checkout/src/handles country/currency/product filtering. Don't bypass this or hardcode availability. -
Thank-you page routing is 800+ lines with 20+ branches — Routing logic is in
get-thank-you-page-url/index.tswith exhaustive unit tests. The code explicitly warns: "IF YOU CHANGE THIS FUNCTION ALSO CHANGE THE TESTS." Don't add new thank-you routes without updating both. -
navigate()silently fails for/setup/routes —navigate()usespage.show()which doesn't work for Stepper routes. Usewindow.location.hreffor cross-origin or setup redirects. Seesrc/lib/leave-checkout.ts. -
3DS challenges not guaranteed — Stripe 3D Secure may or may not trigger. Processors must handle both paths (direct success and challenge flow).
-
Gift purchases bypass everything — Check
cart.is_gift_purchasefirst. Gift purchases skip all upsell logic and route to a separate thank-you page. -
Atomic sites use
.wpcomstaging.com— Thank-you URL logic replaces.wordpress.comwith.wpcomstaging.comfor Atomic sites. -
Siteless purchases — Some products (Akismet, Jetpack, Marketplace) use temporary sites (
siteless.{jetpack|akismet|marketplace.wp|a4a}.com). Guard withpurchase.isAttachedToHoldingSite. Never query site data for these. -
Transferred purchases — Always check ownership before allowing purchase actions.