Checkout

Streamlined purchasing experience in WooNooW with automated session hydration, dynamic payment gateway contracts, auto-registration, multi-currency validation, and fail-closed checkout security.

Overview

The WooNooW checkout is a reactive Single Page Application (SPA) that synchronizes directly with the authoritative WooCommerce cart and session engine. It supports guest checkout, automated customer account provisioning, multi-currency settlement, dynamic shipping rate calculation, invisible CAPTCHA verification, direct-buy funnel bypassing, and subscription renewal collection.

graph TD
    A[Cart Review or Direct Buy Offer] --> B[GET /checkout/bootstrap]
    B --> C[Customer & Address Details]
    C --> D[Shipping Rate Calculation]
    D --> E[Dynamic Payment Gateway Evaluation]
    E --> F[Invisible CAPTCHA Token Generation]
    F --> G[POST /checkout/submit]
    G -->|Validation or Lock Failed| H[Preserve Cart & Reset CAPTCHA]
    G -->|Custom Fields Gateway (payment_handoff)| L[Preserve Cart & Redirect to Native Pay URL]
    G -->|Gateway Payment Failed| M[Preserve Payable Order & Cart for Retry]
    G -->|Payment Succeeded| I[Empty Cart & Commit Session]
    I -->|Guest Auto-Registered| J[Hard Page Reload to Order Received]
    I -->|Existing / Logged In| K[SPA Navigate to Order Received]

Checkout Flow Lifecycle

Step 1: Bootstrap & Session Hydration

When the customer enters the checkout screen, the frontend issues a non-cacheable request to initialize personalized checkout state:

http
GET /wp-json/woonoow/v1/checkout/bootstrap
Cache-Control: no-store, no-cache, must-revalidate, max-age=0

The bootstrap response delivers:

  • Authoritative Cart Totals: Hydrated from the customer's server session (items, subtotal, discount_total, tax_total, total, coupons).
  • Active Currency: Base currency, active currency code, symbol, formatting decimals, and exchange rate ID.
  • Dynamic Checkout Fields: Standard and custom fields, respecting visibility rules and third-party addon filters.
  • Geographic Data: Allowed billing/shipping countries, localized states, and default store country.
  • Saved Addresses: Pre-populated address book entries if the user is authenticated.
  • Payment Gateways: Available active gateways with capability and behavior metadata.
  • Public Security Config: The configured CAPTCHA provider (none, recaptcha, turnstile) and public site keys.

Step 2: Customer Details & Guest Auto-Registration

Customers provide billing contact information, shipping destinations, and order notes.

Automated Customer Registration

Auto-registration removes account-creation friction for new buyers. It is configured in WooNooW → Settings → Customers using the auto_register_members option (default: disabled).

When enabled and a guest submits an order with a billing email:

flowchart TD
    Submit[Guest Submits Order] --> Check{Email Exists in WP?}
    Check -->|Yes| LoginRequired[Reject Order: login_required - Fail Closed]
    LoginRequired --> PreserveCart[Preserve Cart & Prompt Login]
    Check -->|No| CreateUser[Create User via wp_insert_user]
    CreateUser --> GenPass[Generate Secure Password]
    GenPass --> SetCookie[Set Auth Cookie & Current User]
    SetCookie --> FireHook[Fire woocommerce_created_customer Hook]
    FireHook --> RespLogin[Return user_logged_in: true]
  1. Existing User Email (login_required): If the billing email matches an existing registered WordPress account (get_user_by('email', $email)), WooNooW strictly rejects the order submission BEFORE creating an order:
    json
    {
      "ok": false,
      "error": "An account with this email address already exists. Please log in to complete your purchase.",
      "error_code": "login_required"
    }
    
    • Security Protection: Prevents guest impersonation, customer account takeover, and unauthorized linking of order histories. The customer must log in with their account credentials to proceed.
    • Cart Preserved: The customer's cart and applied coupons remain intact while they log in.
  2. New User Email:
    • A new WordPress user account is created with role customer.
    • A secure 12-character random password is generated and stored temporarily in user meta _woonoow_temp_password so WooCommerce can send the standard welcome email with credentials.
    • The customer is automatically authenticated via wp_set_auth_cookie() and wp_set_current_user().
    • The response returns user_logged_in: true.

Frontend Auth Refresh Navigation

When user_logged_in: true is returned, client-side SPA routing is intentionally bypassed:

javascript
// customer-spa/src/pages/Checkout/index.tsx
if (data.user_logged_in) {
  // Perform hard page reload so the browser recognizes the newly set auth cookie
  window.location.href = getSpaUrl(`/checkout/order-received/${data.order_id}?key=${encodeURIComponent(data.order_key)}`);
} else {
  // Standard SPA navigation for existing sessions
  navigate(`/checkout/order-received/${data.order_id}?key=${encodeURIComponent(data.order_key)}`, { replace: true });
}

This ensures the browser's cookie jar is refreshed across the entire site, giving the customer instant access to their account dashboard and order history.


Step 3: Dynamic Shipping Rates

If physical products are in the cart (requires_shipping === true), the client requests available delivery options:

http
POST /wp-json/woonoow/v1/checkout/shipping-rates
Content-Type: application/json

{
  "shipping": {
    "country": "US",
    "state": "NY",
    "city": "New York",
    "postcode": "10001"
  },
  "items": [...]
}
  • Hook Integration: Fires woonoow/shipping/before_calculate allowing shipping addons (such as Rajaongkir or Biteship) to register session destination data.
  • Zone Matching: Matches package destination against WooCommerce shipping zones (WC_Shipping_Zones).
  • Custom Fallback: For external shipping calculators that do not use WooCommerce shipping rate instances, the submission endpoint accepts fallback parameters shipping_cost and shipping_title.

Step 4: Multi-Currency & Coupon Verification

Before order creation, WooNooW performs multi-currency validation:

  1. Exchange Rate Freshness: If the checkout currency differs from the store base currency, the system verifies that the exchange rate snapshot has not expired. Stale exchange rates abort submission with error code currency_rate_stale.
  2. Gateway Compatibility: Verifies that the selected payment method is permitted for the active currency (GatewayCurrencyMapper). Incompatible gateways return currency_gateway_mismatch; unmapped gateways return currency_gateway_unavailable.
  3. Coupon Currency Policy: Coupons restricted to specific currencies are validated (CouponCurrencyPolicy). Invalid coupons return coupon_currency_mismatch.
  4. Order Audit Snapshot: During woocommerce_checkout_create_order, an immutable multi-currency snapshot is saved to the order before totals are finalized.

Step 5: Checkout Security, Concurrency & Advisory Locks

To prevent automated order stuffing, race conditions, and fraudulent card testing, the checkout incorporates three security and concurrency layers:

  1. IP Sliding-Window Rate Limiting: If an IP exceeds woonoow_rate_limit_orders within woonoow_rate_limit_minutes, submission is rejected with:
    json
    { "error": "Too many orders. Please try again later." }
    
  2. Invisible CAPTCHA Verification:
    • Cloudflare Turnstile or Google reCAPTCHA v3 executes silently before order submission.
    • If CAPTCHA is required, a valid captcha_token must be present.
    • Tokens are strictly single-use and fail-closed.
    • On any submission failure (such as a declined card, coupon validation error, or server error), the client immediately discards the token and resets the widget to prevent replay attacks.
  3. Fail-Closed Per-Session Advisory Lock (wn_buy_ + md5):
    • Serializes checkout submission per active session using MySQL advisory lock wn_buy_ . md5($customer_session_id) with a 5-second timeout (CheckoutController.php:365-374).
    • This lock is shared exclusively between POST /cart/buy and POST /checkout/submit; it is NOT acquired by native WooCommerce AJAX requests (/?wc-ajax=...).
    • Reloads session data from the database under lock (BuyIntent::refresh_session()) before evaluating cart items or creating orders, preventing parallel submission race conditions.

Step 6: Dynamic Payment Gateways & Behavior Contract

During checkout bootstrap and order submission, WooNooW classifies payment methods into three clear operational behaviors (includes/Api/CheckoutController.php:1312-1325):

graph TD
    GW[Gateway Selected] --> Check{Has Fields or Offline?}
    Check -->|bacs, cheque, cod| Offline[offline: process_payment and redirect to receipt]
    Check -->|has_fields === true| Hosted[hosted_pay: redirect to native hosted pay URL]
    Check -->|Standard Offsite| Redirect[redirect: redirect to payment processor URL]
BehaviorSupported GatewaysProcessing & Handoff
offlineDirect bank transfer (bacs), Cash on delivery (cod), Cheque (cheque)Order is created directly in WooCommerce. Handled via process_payment(). Order status is set to on-hold or processing, and the customer is routed directly to the /order-received/:id receipt with payment instructions.
redirectStandard offsite redirect methods (e.g. PayPal Standard, external hosted processors)Order is created in WooCommerce. Gateway returns an external authorization URL; the browser is redirected to the payment provider. Upon return, the customer lands on the receipt.
hosted_payGateways with custom inline fields (has_fields === true), such as direct credit card forms or Stripe elementsOrder is created in WooCommerce. To protect customer card details without risky client-side injection, WooNooW securely hands off payment collection to WooCommerce's native query-based hosted pay URL: /?page_id={checkout_id}&order-pay={order_id}&pay_for_order=true&key={key}&woonoow_native_pay=1. Note that the React route /checkout/pay is not native hosted payment.

Payment Handoff Preserves the Cart

When a gateway requiring custom fields is selected (has_fields === true), the submission returns payment_handoff: true:

  • The customer's cart is explicitly PRESERVED in the session so items are not lost if the customer abandons the external hosted payment screen or returns to adjust their order.
  • The cart is emptied when process_payment accepts the order (result === 'success'), or after completing a free order. Offline/redirect acceptance is not proof of settlement; use the receipt status and WooCommerce payment confirmation.

Native Gateway Return URL Filter

When gateways finish processing and return to WooCommerce's order received endpoint:

  • WooNooW hooks into woocommerce_get_checkout_order_received_url (priority 20, 2 args in TemplateOverride.php:20, 90-98).
  • Rewrites the thank-you URL to the reloadable SPA route /checkout/order-received/{id}?key={key} strictly for orders tagged with _woonoow_session_id when spa_mode === 'full'.
  • Standard storefront or non-WooNooW orders without the session tag retain default WooCommerce thank-you URLs.

Thank-You Receipt != Settled Payment

When an order is submitted, the customer is directed to /order-received/:id?key=....

  • The receipt displays the live order status (e.g. on-hold for bank transfers or pending for asynchronous gateways).
  • An order receipt is a transaction acknowledgment, not proof of settled funds. Merchant fulfillment should verify actual payment capture in WooCommerce.
  • Order View & Payment Confirmation (GET /checkout/order/:id in CheckoutController.php:189-297):
    • Validates access via order key (constant-time hash_equals()) OR authenticated customer owner (get_current_user_id() === $order->get_customer_id()) OR administrator (manage_woocommerce).
    • The payment_confirmed boolean:
      • For Cash on Delivery (payment_method === 'cod'), payment_confirmed is true ONLY when order status is completed (NOT processing).
      • For other payment methods, payment_confirmed is true if $order->is_paid() || $order->has_status('completed').

Step 7: Order Placement, Idempotency & Cart Lifecycle

Order submission is processed through POST /woonoow/v1/checkout/submit.

Checkout Idempotency (checkout_intent_id)

  • Optional Intent ID: Clients may submit an optional checkout_intent_id (UUID format matching regex /^[0-9a-fA-F\-]{16,64}$/).
  • Stable Hash Computation: Computes a deterministic payload hash (get_payload_intent_hash()), specifically excluding volatile fields: captcha_token, order_id, order_key, and checkout_intent_id.
  • Session-Bounded Success Replay: Stores up to 50 recent successful checkout intents (woonoow_checkout_intents) in WC()->session.
    • If an identical payload is submitted with the same checkout_intent_id, the server replays the original success response without creating duplicate orders or double-charging.
    • If a differing payload is submitted with an already-used checkout_intent_id, the server aborts with 400 error checkout_intent_conflict.
    • Session Lifetime Boundary: Idempotency is bounded strictly to the customer's active WooCommerce session lifetime; it is not durable beyond session expiration or purge.

Successful Submission Cart Cleanup

When an order is created and payment succeeds (result === 'success'):

  1. WC()->cart->empty_cart(true) clears all items, shipping packages, and coupon records from the server session.
  2. WC()->session->save_data() commits the cleared state to the database.
  3. If a guest user auto-registered during checkout, the previous guest session key ($guest_session_id) is deleted via WC()->session->delete_session($guest_session_id) to prevent orphan session revival.
  4. The frontend resets the client-side cart store and invalidates TanStack React Query caches (cart, account-orders).

Failed Submission & Sequential Retry Contract

If gateway processing fails or throws an exception:

  1. Payable Order Preserved: The created order is preserved in status failed or pending (it is NOT deleted).
  2. Failure Response Contract: The endpoint returns:
    json
    {
      "ok": false,
      "error": "Payment processing failed. Please try again or choose another payment method.",
      "order_id": 123,
      "order_key": "wc_order_abc123xyz",
      "status": "pending",
      "pay_url": "https://example.com/?page_id=...&order-pay=123...",
      "can_retry": true
    }
    
  3. Sequential Retry Reuse: The frontend retries by submitting the same payload augmented with order_id and order_key.
  4. Fingerprint Validation & Per-Order Lock: The controller verifies exact fingerprint match (currency, line items, billing/shipping addresses, coupons, and shipping method) and acquires a dedicated per-order lock wnw_order_pay_{order_id}, safely reusing the existing order rather than creating duplicate unpaid orders.
  5. Cart Preserved: The customer's cart and applied coupons are NOT emptied. The server session and client basket remain intact so the customer can correct payment details without losing items.

Direct Buy Intent (POST /cart/buy) & Sales Funnels

WooNooW provides a high-converting Direct Buy Now path used by the native Purchase Offer section:

sequenceDiagram
    actor Buyer
    participant Section as Purchase Offer Section
    participant API as POST /cart/buy
    participant Checkout as /checkout SPA

    Buyer->>Section: Click "Buy Now"
    Note over Section: If cart_policy === "replace" and cart has items: confirm replacement
    Section->>API: Dispatch safe purchase intent
    API->>API: Validate product, stock & cart policy
    API-->>Section: Return updated cart & checkout_url
    Section->>Checkout: Client-side navigate to /checkout

Request Contract (POST /woonoow/v1/cart/buy)

json
{
  "product_id": 123,
  "variation_id": 0,
  "attributes": {
    "size": "large"
  },
  "quantity": 1,
  "cart_policy": "replace",
  "intent_id": "11111111-1111-4111-8111-111111111111",
  "return_path": "/special-offer"
}
  • product_id (integer, required): Published WooCommerce product ID (must be strictly positive $> 0$).
  • variation_id (integer, optional): Non-negative variation ID for variable products ($\ge 0$, default 0).
  • attributes (object, optional): Canonical key-value map of variation attribute slugs to scalar values. Normalized via BuyIntent::normalize_attributes() (lowercased, trimmed, and sorted alphabetically) for deterministic payload hashing.
  • quantity (integer, required): Units to purchase (strictly positive integer $\ge 1$, default 1).
  • cart_policy (string, required):
    • replace (default): Clears the existing cart before adding this offer. Triggers explicit browser confirmation (window.confirm) if the cart already holds other items.
    • append: Adds this offer alongside existing cart contents.
  • intent_id (string, required): Unique UUID v4 idempotency key matching regex /^[0-9a-fA-F\-]{16,64}$/.
  • return_path (string, optional): Relative local URL. Sensitive query parameters (token, secret, order_key, nonce) are automatically stripped before storage (defaults to /).

Security, Headers & Concurrency Controls

  • Nonce & Origin: Requires valid wp_rest nonce (X-WP-Nonce header or _wpnonce) and same-origin verification.
  • Active Session Cookie: Requires an active customer/guest WooCommerce session cookie.
  • Strict Caching Headers: Sets Cache-Control: no-store, no-cache, must-revalidate, max-age=0 via nocache_headers().
  • Shared Advisory Lock (wn_buy_ + md5): Acquires fail-closed MySQL advisory lock with 5s timeout (configurable via filter woonoow_buy_intent_lock_timeout). Shared exclusively between POST /cart/buy and POST /checkout/submit (not native WooCommerce AJAX). Test simulation filter: woonoow_buy_intent_simulate_lock_failure.
  • Session Refresh Under Lock: Refreshes session data from database (BuyIntent::refresh_session()) to eliminate stale parallel state.
  • Atomic Mutation & Rollback: Cart snapshot (snapshot_cart()) restored on any validation or addition failure (restore_cart_from_snapshot()). Evaluates standard WooCommerce add-to-cart filter woocommerce_add_to_cart_validation.
  • Session-Bounded Idempotency: Remembers up to 50 recent buy intents (woonoow_buy_intents) in WC()->session. Identical retries return cached result; conflicting payload hashes return 409 Conflict (intent_conflict). Not durable beyond active WooCommerce session lifetime.

Variable Products & WooCommerce "Any"

If a product has variations where attributes are set to "Any", WooNooW automatically resolves a non-empty allowed attribute selection (defaulting to the preset variation or first available option), which the buyer can review or adjust before clicking Buy.

Funnel Bypass UX vs The Cart Engine

Direct Buy Now bypasses the customer-facing /cart interface for friction-free purchasing, but it operates through WooCommerce's underlying cart and session engine.

  • Do NOT Delete Standard WooCommerce Pages: Never delete /cart, /checkout, or /my-account. They are required for cart session hydration, tax computation, and order completion.

Pay-Order Flow & Subscription Renewals

WooNooW provides a dedicated payment processing endpoint for existing orders, including manual renewals and unpaid invoices:

http
POST /wp-json/woonoow/v1/checkout/pay-order/{id}
Content-Type: application/json

{
  "key": "wc_order_abc123xyz",
  "payment_method": "stripe"
}

Access Control

Access to pay an existing order is strictly validated:

  • Order Key Validation: For guests, key must match $order->get_order_key() using constant-time hash_equals().
  • Authenticated Ownership: Logged-in customers can pay orders where get_current_user_id() === $order->get_customer_id().
  • Administrative Privileges: Store administrators with manage_woocommerce capability can initiate payment on any order.

Renewal Payment Window Enforcement

When processing payment for a subscription renewal order (order_type === 'renewal'):

  1. The controller checks SubscriptionManager::is_renewal_payment_window_open($order).
  2. If the window has expired, renewal orders are transitioned to cancelled and the customer is prompted to purchase a new subscription.

Architectural Boundaries & Practical Guidance

Session Lifetime Limits & Durability Realities

  • Session-Bounded Idempotency: Idempotency records for both /cart/buy and /checkout/submit are stored within WooCommerce's session cache (woonoow_buy_intents and woonoow_checkout_intents, capped at 50 entries each). They do not persist across session purge, cookie expiration, or customer session abandonment.
  • No Blanket Claims: WooNooW does not claim durability beyond the WooCommerce session lifetime, nor does it guarantee exactly-once payment processing across arbitrary third-party payment gateways.

Root, Entry, and Landing Page SEO Alignment

  • In Full SPA Mode, client-side routing dynamically serves checkout and receipt views.
  • Recommendation: Configure your store root (/), entry route, and marketing landing pages to the same page (e.g. frontpage). This prevents crawler confusion and canonical link fragmentation.

Manual Sandbox Gateway Acceptance Testing

  • Third-party payment gateways handle inline fields, redirects, webhooks, and 3D Secure / SCA differently. WooNooW does not claim arbitrary third-party payment SDKs can run inline in React without testing.
  • Mandatory Testing: Always conduct manual end-to-end sandbox acceptance testing for each active payment gateway (including card declines, payment cancellations, and return redirects) prior to production deployment.

Last updated Sep 14, 2026