Skip to content

Checkout ​

The checkout component manages the entire checkout flow: collecting customer details, selecting shipping and payment methods, placing orders, and processing payments. It stores checkout state in the session and provides AJAX handlers for each step.

Component Declaration ​

ini
[checkout]

The Checkout component has no configurable properties. Add it to your checkout page.

Checkout Modes ​

The component supports two address models. The mode is detected automatically from what your theme posts, and it is sticky for the checkout session (readable through the checkout.hasSeparateAddresses() method).

Unified address (contact only) ​

The theme collects a single address block using the unprefixed contact fields (first_name, address_line1, and so on) via post_contact_details. As long as no shipping address is posted, the checkout stays unified: the shipping and billing addresses both fall back to these contact details, so the one unified address is used for everything. Billing fields are never required in this mode. This suits digital stores or any checkout that does not need separate addresses.

Separate addresses (shipping and billing) ​

The checkout switches to separate-address mode as soon as the theme posts a distinct address, and stays there. Either address can establish it:

  • Posting a shipping address (shipping_ prefixed fields via post_shipping_details) enters separate mode. Billing then reuses the shipping address unless the customer opts out by posting billing_same_as_shipping = 0, which reveals a separate billing address (billing_ prefixed fields via post_billing_details). This is the familiar retail "Billing address same as shipping" pattern.
  • Posting a distinct billing address (billing_ fields with billing_same_as_shipping = 0) also enters separate mode, even when no shipping address has been posted. This suits a "Bill to a different address" checkout where billing is the address that leads.

billing_same_as_shipping defaults to false, so posting billing_ fields means a distinct billing address: its fields are validated as required. The "same as shipping" option only applies when a shipping address was entered first; when billing leads, there is nothing to reuse and the billing address simply stands on its own. Selecting a saved billing address book entry also puts the checkout into separate mode.

The billing address resolves through the chain billing → shipping → contact, so a partially completed checkout still produces a valid billing address.

Page Variables ​

The component sets these page variables on every page load and after each AJAX request. Use them in your Twig templates to display checkout state.

Order Preview ​

VariableTypeDescription
orderOrderA mock order calculated from the current cart and checkout data (not saved to database)
invoiceInvoiceThe mock order's associated invoice
itemsCartItemCollectionActive cart items

Contact and Billing ​

VariableTypeDescription
addressCheckoutAddressThe customer's contact details
billingAddressCheckoutAddressThe billing address. In separate mode it follows the shipping address unless a separate billing address is entered; in unified mode it follows the contact details
billingSameAsShippingbooleanWhether the billing address reuses the shipping address (false by default). Only meaningful in separate mode; true makes billingAddress follow the shipping address
paymentMethodPaymentMethod|nullThe currently selected payment method
paymentMethodsCollectionPayment methods available for this order
hasPaymentMethodbooleanWhether a payment method is selected
allPaymentMethodsCollectionAll enabled payment methods

Store Credit ​

VariableTypeDescription
creditBalanceintThe logged-in customer's available store credit balance (0 if credit is disabled or guest)

Coupon ​

VariableTypeDescription
couponCodestringThe currently applied coupon code

Shipping ​

VariableTypeDescription
shippingRequiredbooleanWhether the cart contains shippable products
shippingMethodsarrayShipping methods available for the current address
shippingAddressCheckoutAddressThe shipping address (falls back to contact details if not set separately)
shippingMethodShippingMethod|nullThe selected shipping method with quote
hasShippingAddressbooleanWhether a shipping location has been provided
hasShippingMethodbooleanWhether a shipping method is selected
hasShippingQuotebooleanWhether both address and method are set (quote is available)

Twig API ​

checkout.user() ​

Returns the currently logged-in user, or null for guest checkout.

checkout.isCartEmpty() ​

Returns true if the cart has no items. Use this to show an empty cart message instead of the checkout form.

checkout.hasSeparateAddresses() ​

Returns true once the checkout is in separate-address mode (a distinct shipping or billing address has been posted) rather than using a single unified contact address. The mode is sticky for the checkout session, so a toggle bound to this keeps its state across AJAX refreshes.

AJAX Handlers ​

onRefreshCheckout ​

The primary checkout handler. It processes any pending checkout step data from the POST request, then refreshes the page variables. This is the handler used for navigating between checkout steps.

When called, onRefreshCheckout processes all post_* flags found in the request (see POST Parameters below), updates the checkout session, and returns the updated page variables for partial rendering.

html
<button
    data-request="onRefreshCheckout"
    data-request-data="{ checkout_step: 'shipping' }"
    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }">
    Continue to Shipping
</button>

If the cart contents have changed since the checkout started, onRefreshCheckout triggers a page refresh to recalculate totals. If the order has already been paid, it redirects to the receipt page.

onPlaceOrder ​

Creates the order, generates an invoice, and redirects the customer to the payment page. This is the final step before payment.

html
<button
    data-request="checkout::onPlaceOrder"
    class="btn btn-primary btn-lg">
    Place Order
</button>

Before creating the order, this handler processes any checkout step data in the request (contact details, addresses, methods). This allows you to submit all checkout data in a single request if desired.

If store credit covers the full order total (i.e., amount_due is zero), the order is marked as paid immediately and the customer is redirected to the receipt page, bypassing the payment gateway entirely.

Returns: A redirect to the payment page by default (or receipt page if fully covered by credit). If no_redirect is posted, returns a JSON object instead:

json
{
    "order_hash": "abc123...",
    "invoice_hash": "def456...",
    "return_url": "/shop/payment/def456..."
}
ParameterTypeDescription
no_redirectbooleanReturn JSON instead of redirecting

onPrepareOrder ​

Creates the order without emptying the cart or finalizing. This is used for API integrations where the order may need amendments before payment.

Returns: A JSON object:

json
{
    "order_hash": "abc123...",
    "invoice_hash": "def456..."
}

The order hash is stored in the session so subsequent calls to onPlaceOrder or onPrepareOrder update the existing order rather than creating a new one.

onPay ​

Processes the payment form submission on the payment page. This handler is used after the order has been placed and the customer is on the payment page.

ParameterTypeDescription
pay_from_profilebooleanUse the customer's stored payment method instead of the posted form data
html
{# Standard payment form #}
<form data-request="checkout::onPay">
    <!-- Payment form fields rendered by the payment method driver -->
    <button type="submit">Pay Now</button>
</form>

{# Pay with stored payment method #}
<button
    data-request="checkout::onPay"
    data-request-data="{ pay_from_profile: true }">
    Pay with Stored Card
</button>

On successful payment, the customer is redirected to the receipt page. The payment method driver may also return a custom redirect (e.g., to a third-party payment gateway).

onApplyCredit ​

Applies the logged-in customer's store credit to the order. The amount applied is the lesser of the customer's available balance and the order total. Requires store credit to be enabled in the Responsiv.Pay settings and the customer to be logged in.

html
<button
    data-request="checkout::onApplyCredit"
    data-request-update="{ 'shop-checkout/order-summary': '#shopCheckoutOrderSummary' }">
    Apply Store Credit
</button>

If the applied credit covers the full order total, onPlaceOrder will skip the payment gateway and mark the order as paid automatically.

onRemoveCredit ​

Removes store credit from the current checkout session.

html
<button
    data-request="checkout::onRemoveCredit"
    data-request-update="{ 'shop-checkout/order-summary': '#shopCheckoutOrderSummary' }">
    Remove Store Credit
</button>

POST Parameters ​

The checkout component processes POST parameters to update checkout state. Each parameter group is triggered by a corresponding flag.

Contact Details ​

Triggered by post_contact_details = true.

ParameterTypeDescription
first_namestringCustomer first name
last_namestringCustomer last name
emailstringEmail address
phonestringPhone number
companystringCompany name
address_line1stringStreet address
address_line2stringApartment, suite, etc.
citystringCity
zipstringPostal code
country_idintegerCountry ID
state_idintegerState ID
tax_id_numberstringTax ID / VAT number

Address Book Preset ​

Triggered by post_address_book_preset = true. Loads a saved address from the customer's address book into the contact details (requires RainLab.UserPlus). The selected address_book_id is remembered across AJAX refreshes rather than resetting to "New address".

ParameterTypeDescription
address_book_idintegerThe saved address ID to load

Billing Address Book Preset ​

Triggered by post_billing_address_book_preset = true. Loads a saved address from the customer's address book into the billing address, mirroring the contact address book preset (requires RainLab.UserPlus). Pass an empty value to reset the billing address to a new entry.

ParameterTypeDescription
billing_address_book_idintegerThe saved address ID to load as the billing address

Billing Details ​

Triggered by post_billing_details = true. Uses the same fields as contact details with a billing_ prefix.

ParameterTypeDescription
billing_first_namestringBilling first name
billing_last_namestringBilling last name
billing_emailstringBilling email
billing_phonestringBilling phone
billing_companystringBilling company
billing_address_line1stringBilling street address
billing_address_line2stringBilling apartment, suite, etc.
billing_citystringBilling city
billing_zipstringBilling postal code
billing_country_idintegerBilling country ID
billing_state_idintegerBilling state ID
billing_tax_id_numberstringBilling tax ID / VAT number
billing_same_as_shippingbooleanWhether the billing address reuses the shipping address (1) or a separate billing address is being entered (0); defaults to 0

Posting billing details puts the checkout into separate-address mode. Post billing_same_as_shipping = 1 to reuse a shipping address that was entered first; the billing_ fields are then ignored. When it is 0 (the default), the billing_ fields are validated as required and stored as a distinct billing address, whether or not a shipping address exists yet. The flag is stored on the checkout session and exposed as the billingSameAsShipping page variable, so a "Billing address is the same as shipping" toggle keeps its state across AJAX refreshes. Selecting a billing address book preset also switches to separate-address mode automatically.

Shipping Details ​

Triggered by post_shipping_details = true. Uses the same fields as contact details with a shipping_ prefix.

ParameterTypeDescription
shipping_first_namestringShipping first name
shipping_last_namestringShipping last name
shipping_companystringShipping company
shipping_phonestringShipping phone
shipping_address_line1stringShipping street address
shipping_address_line2stringShipping apartment, suite, etc.
shipping_citystringShipping city
shipping_zipstringShipping postal code
shipping_country_idintegerShipping country ID
shipping_state_idintegerShipping state ID
shipping_is_businessbooleanBusiness address flag

If shipping details are not explicitly submitted, the shipping address inherits from the contact details. Posting a shipping address switches the checkout to separate-address mode.

Shipping Address Book Preset ​

Triggered by post_shipping_address_book_preset = true. Loads a saved address from the customer's address book into the shipping address, mirroring the contact and billing address book presets (requires RainLab.UserPlus). It also switches the checkout to separate-address mode. Pass an empty value to reset the shipping address to a new entry.

ParameterTypeDescription
shipping_address_book_idintegerThe saved address ID to load as the shipping address

Shipping Method ​

Triggered by post_shipping_method = true.

ParameterTypeDescription
shipping_methodintegerThe shipping method ID. For methods with child options, this may include the child option identifier

Payment Method ​

Triggered by post_payment_method = true.

ParameterTypeDescription
payment_methodintegerThe payment method ID

Additional Parameters ​

These parameters are processed on every request, without requiring a flag:

ParameterTypeDescription
user_notesstringOrder notes or special instructions
couponstringCoupon code to apply
cart_namestringCart name (default: main)
skip_validationbooleanDisable validation for partial/eager updates
checkout_stepstringUsed by templates to track which step is displayed

Guest Registration ​

These parameters are processed during order placement:

ParameterTypeDescription
register_userbooleanCreate a customer account for the guest
user_passwordstringPassword for the new account
user_auto_loginbooleanAuto-login after registration
user_register_notificationbooleanSend registration email

Checkout Flow ​

The Commerce Theme implements a three-step checkout flow. Each step submits its data to the onRefreshCheckout handler, which stores it in the session and returns updated page variables.

Step 1: Contact Details & Address
    ↓ post_contact_details = true
Step 2: Shipping Method
    ↓ post_shipping_method = true
Step 3: Payment Method
    ↓ post_payment_method = true
Place Order → onPlaceOrder
    ↓ Redirect to payment page
Payment Page → onPay
    ↓ Redirect to receipt

For carts that contain only digital products, shippingRequired is false and the shipping step is skipped automatically. A special "No Shipping Required" method is set behind the scenes.

Step Navigation with skip_validation ​

When navigating backward between steps (e.g., "Return to Details"), pass skip_validation: true to prevent validation errors on the current step's incomplete data:

html
<a href="javascript:;"
    data-request="onRefreshCheckout"
    data-request-data="{ checkout_step: 'details', skip_validation: true }"
    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }">
    Return to Details
</a>

Cart Integrity ​

The component detects when cart contents change during checkout (items added/removed in another tab). When this happens, the checkout state is reset and the page is refreshed to recalculate totals. This prevents the customer from placing an order with stale pricing.

Similarly, if the order has already been paid (e.g., the customer navigates back to the checkout page after paying), the component redirects to the receipt page.

Complete Examples ​

Checkout Page ​

twig
{# pages/shop/checkout.htm #}
##
url = "/shop/checkout"
layout = "default"
title = "Checkout"

[checkout]
==
{% if not checkout.isCartEmpty %}
    {% put scripts %}
        {% for method in allPaymentMethods %}
            {{ method.renderPaymentScripts()|raw }}
        {% endfor %}
    {% endput %}
    <div id="shopCheckoutView">
        {% partial 'shop/checkout-view' %}
    </div>
{% else %}
    <div class="container">
        <h1>Checkout</h1>
        <p>Your shopping cart is empty.</p>
        <a href="{{ 'shop/index'|page }}" class="btn btn-primary">Continue Shopping</a>
    </div>
{% endif %}

WARNING

Payment method scripts must be loaded regardless of which step the customer is on. The renderPaymentScripts() call loads JavaScript required by payment gateways (e.g., Stripe.js) that may need to be initialized before the payment form is displayed.

Step Router ​

The checkout view partial acts as a step router, displaying different content based on the checkout_step POST value:

twig
{# partials/shop/checkout-view.htm #}
{% set checkoutStep = post('checkout_step', 'details') %}

<div class="row">
    <div class="col-lg-7">
        <form id="checkoutForm" data-control="checkout-form" data-request-flash>
            {% if checkoutStep == 'details' %}
                {% partial 'shop/checkout-step-details' %}
                <input type="hidden" name="post_contact_details" value="true" />
                <button
                    data-request="onRefreshCheckout"
                    data-request-data="{ checkout_step: 'shipping' }"
                    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }"
                    data-attach-loading
                    class="btn btn-primary btn-lg">
                    Continue to Shipping
                </button>

            {% elseif checkoutStep == 'shipping' %}
                {% partial 'shop/checkout-step-shipping' %}
                <input type="hidden" name="post_shipping_method" value="true" />
                <a href="javascript:;"
                    data-request="onRefreshCheckout"
                    data-request-data="{ checkout_step: 'details', skip_validation: true }"
                    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }">
                    Return to Details
                </a>
                <a href="javascript:;"
                    data-request="onRefreshCheckout"
                    data-request-data="{ checkout_step: 'payment' }"
                    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }"
                    data-attach-loading
                    class="btn btn-primary btn-lg">
                    Continue to Payment
                </a>

            {% elseif checkoutStep == 'payment' %}
                {% partial 'shop/checkout-step-payment' %}
                <input type="hidden" name="post_payment_method" value="true" />
                <a href="javascript:;"
                    data-request="onRefreshCheckout"
                    data-request-data="{ checkout_step: 'shipping', skip_validation: true }"
                    data-request-update="{ 'shop/checkout-view': '#shopCheckoutView' }">
                    Return to Shipping
                </a>
            {% endif %}
        </form>

        {% if checkoutStep == 'payment' %}
            <div id="shopPaymentForm">
                {% partial 'shop/payment-form' %}
            </div>
        {% endif %}
    </div>
    <div class="col-lg-5" id="shopCheckoutOrderSummary">
        {% partial 'shop/order-summary' %}
    </div>
</div>

Contact Details Step ​

twig
{# partials/shop/checkout-step-details.htm #}
<div class="d-flex align-items-center mb-2">
    <h4 class="m-0">Contact Details</h4>
    {% if not checkout.user %}
        <div class="ms-auto small">
            Already have an account?
            <a href="{{ 'account/login'|page }}">Sign in first</a>
        </div>
    {% endif %}
</div>

{% if checkout.user %}
    <div class="alert alert-light">
        Bill to <strong>{{ checkout.user.full_name }}, {{ checkout.user.email }}</strong>
        <a href="javascript:;" data-request="onLogout" class="ms-auto">Logout</a>
    </div>
{% else %}
    <div class="form-floating mb-2">
        <input name="email" type="text" class="form-control"
            value="{{ address.email }}" placeholder="Email Address" required />
        <label>Email Address</label>
    </div>
    <label>
        <input type="checkbox" name="register_customer" value="1" />
        Save my details for faster checkout
    </label>
{% endif %}

<h4>{{ shippingRequired ? 'Shipping Details' : 'Billing Details' }}</h4>

{% if checkout.user %}
    <div class="form-floating">
        {% partial 'account/select-address'
            user=checkout.user
            addressBookId=address.address_book_id %}
        <label>Address Book</label>
    </div>
{% endif %}

<div class="row g-3">
    <div class="col-6">
        <div class="form-floating">
            <input name="first_name" type="text" class="form-control"
                value="{{ address.first_name }}" placeholder="First Name" required />
            <label>First Name</label>
        </div>
    </div>
    <div class="col-6">
        <div class="form-floating">
            <input name="last_name" type="text" class="form-control"
                value="{{ address.last_name }}" placeholder="Last Name" required />
            <label>Last Name</label>
        </div>
    </div>
    <div class="col-12">
        <div class="form-floating">
            <input name="address_line1" type="text" class="form-control"
                value="{{ address.address_line1 }}" placeholder="Address" />
            <label>Address</label>
        </div>
    </div>
    <div class="col-md-6">
        <div class="form-floating">
            <input name="city" type="text" class="form-control"
                value="{{ address.city }}" placeholder="City" />
            <label>City</label>
        </div>
    </div>
    <div class="col-md-6">
        <div class="form-floating">
            <input name="zip" type="text" class="form-control"
                value="{{ address.zip }}" placeholder="Zip / Postal Code" />
            <label>Zip / Postal Code</label>
        </div>
    </div>
    <div class="col-12">
        <div class="form-floating">
            {% partial 'account/select-country' countryId=address.country_id %}
            <label>Country</label>
        </div>
    </div>
    <div class="col-12">
        <div class="form-floating">
            {% partial 'account/select-state'
                countryId=address.country_id
                stateId=address.state_id %}
            <label>State</label>
        </div>
    </div>
    <div class="col-12">
        <div class="form-floating">
            <input name="phone" type="text" class="form-control"
                value="{{ address.phone }}" placeholder="Phone" />
            <label>Phone</label>
        </div>
    </div>
</div>

Shipping Method Step ​

twig
{# partials/shop/checkout-step-shipping.htm #}
<h4>Shipping Method</h4>

{% if hasShippingAddress %}
    {% if shippingMethods is not empty %}
        <ul class="list-group mt-3">
            {% for method in shippingMethods %}
                {% if method.hasChildOptions %}
                    {% for childOption in method.childOptions %}
                        <li class="list-group-item">
                            <label class="d-flex w-100">
                                <input type="radio" name="shipping_method"
                                    value="{{ childOption.id }}"
                                    {{ shippingMethod.id == childOption.id ? 'checked' }} />
                                <div class="ps-2">
                                    <p class="mb-0">{{ method.name }} - {{ childOption.name }}</p>
                                    {% if method.description %}
                                        <small class="text-muted">{{ method.description }}</small>
                                    {% endif %}
                                </div>
                                <div class="ms-auto fw-bold">
                                    {{ childOption.quote|currency }}
                                </div>
                            </label>
                        </li>
                    {% endfor %}
                {% else %}
                    <li class="list-group-item">
                        <label class="d-flex w-100">
                            <input type="radio" name="shipping_method"
                                value="{{ method.id }}"
                                {{ shippingMethod.id == method.id ? 'checked' }} />
                            <div class="ps-2">
                                <p class="mb-0">{{ method.name }}</p>
                                {% if method.description %}
                                    <small class="text-muted">{{ method.description }}</small>
                                {% endif %}
                            </div>
                            <div class="ms-auto fw-bold">
                                {% if method.isFree %}
                                    Free
                                {% else %}
                                    {{ method.quote|currency }}
                                {% endif %}
                            </div>
                        </label>
                    </li>
                {% endif %}
            {% endfor %}
        </ul>
    {% else %}
        <p>No shipping methods available for your location.</p>
    {% endif %}
{% else %}
    <p>Enter your shipping address to view available shipping methods.</p>
{% endif %}

Payment Method Step ​

twig
{# partials/shop/checkout-step-payment.htm #}
<h4>Payment Method</h4>

{% if paymentMethods is not empty %}
    <ul class="list-group mt-3">
        {% for method in paymentMethods %}
            <li class="list-group-item">
                <label class="d-flex w-100">
                    <input type="radio" name="payment_method"
                        value="{{ method.id }}"
                        {{ paymentMethod.id == method.id ? 'checked' }} />
                    <div class="ps-2">
                        <p class="mb-0">{{ method.name }}</p>
                        {% if method.description %}
                            <small class="text-muted">{{ method.description }}</small>
                        {% endif %}
                    </div>
                </label>
            </li>
        {% endfor %}
    </ul>
{% else %}
    <p>No payment methods available.</p>
{% endif %}

Order Summary ​

twig
{# partials/shop/order-summary.htm #}
<ul class="list-group list-group-flush">
    {% for item in items %}
        {% set product = item.product %}
        <li class="list-group-item py-3">
            <div class="row align-items-center">
                <div class="col-3">
                    <div class="position-relative">
                        {% if product.images is not empty %}
                            <img class="img-fluid"
                                src="{{ product.images.first|resize(0, 100, { mode: 'auto' }) }}"
                                alt="{{ product.name }}" />
                        {% endif %}
                        <span class="badge rounded-pill bg-secondary position-absolute top-0 start-100 translate-middle">
                            {{ item.quantity }}
                        </span>
                    </div>
                </div>
                <div class="col-6">
                    <h5 class="mb-1">{{ product.name }}</h5>
                    <div class="small">
                        {% for option in item.options %}
                            {{ option.name }}: {{ option.value }}{{ not loop.last ? ', ' }}
                        {% endfor %}
                        {% for extra in item.extras %}
                            <br />+ {{ extra.description }}
                        {% endfor %}
                    </div>
                </div>
                <div class="col-3 text-end small">
                    {{ item.display_line_price|currency }}
                </div>
            </div>
        </li>
    {% endfor %}
</ul>

<div class="card">
    <ul class="list-group list-group-flush">
        <li class="list-group-item d-flex justify-content-between small">
            <span>Subtotal</span>
            <span>{{ order.final_subtotal|currency }}</span>
        </li>
        <li class="list-group-item d-flex justify-content-between small">
            <span>Shipping</span>
            {% if not hasShippingAddress %}
                <small class="text-muted">Enter address</small>
            {% elseif not hasShippingQuote %}
                <small class="text-muted">Select shipping</small>
            {% else %}
                <span>{{ order.final_shipping_quote|currency }}</span>
            {% endif %}
        </li>
        <li class="list-group-item d-flex justify-content-between">
            <span class="fw-bold">Total</span>
            <span class="fw-bold fs-5">{{ order.total|currency }}</span>
        </li>
        <li class="list-group-item small">
            Includes {{ order.total_tax|currency }} in taxes
        </li>
    </ul>
</div>

Payment Page ​

After the order is placed, the customer is redirected to the payment page. This page uses the [payment] component from Responsiv.Pay:

twig
{# pages/shop/payment.htm #}
##
url = "/shop/payment/:hash"
layout = "default"
title = "Order"

[payment]
isDefault = 1
==
{% set order = invoice.related %}
{% if not order or not order.order_number %}
    {% do abort(404) %}
{% endif %}

{% put scripts %}
    {% for method in paymentMethods %}
        {{ method.renderPaymentScripts()|raw }}
    {% endfor %}
{% endput %}

<div class="container">
    <div class="row">
        <div class="col-lg-7">
            <p>Order #{{ order.order_number }}</p>
            <h2>Thank you, {{ order.billing_first_name }}!</h2>

            {% if order.is_payment_processed %}
                <div class="card mb-3">
                    <div class="card-body">
                        <h5>Your Order is Confirmed</h5>
                        <p>We have processed your payment and accepted your order.</p>
                    </div>
                </div>
            {% else %}
                <div class="card mb-3">
                    <div class="card-body">
                        <h5>Your Order is Pending</h5>
                        <p>Please use a payment method below to complete this order.</p>
                    </div>
                </div>

                {% if paymentMethods is not empty %}
                    <ul class="list-group mb-3">
                        {% for method in paymentMethods %}
                            <li class="list-group-item">
                                <label class="d-flex w-100">
                                    <input type="radio" name="payment_method"
                                        value="{{ method.id }}"
                                        data-request="onUpdatePaymentMethod"
                                        data-request-update="{
                                            'shop/payment-form': '#shopPaymentForm',
                                            'shop/order-details': '#shopOrderDetails'
                                        }"
                                        {{ order.payment_method_id == method.id ? 'checked' }} />
                                    <div class="ps-2">{{ method.name }}</div>
                                </label>
                            </li>
                        {% endfor %}
                    </ul>
                {% endif %}

                <div id="shopPaymentForm">
                    {% partial 'shop/payment-form' %}
                </div>
            {% endif %}
        </div>
        <div class="col-lg-5" id="shopCheckoutOrderSummary">
            {% partial 'shop/order-summary' items=order.items %}
        </div>
    </div>
</div>

The checkout-form.js Control ​

The Commerce Theme includes a checkout-form.js JavaScript control that enhances the checkout experience. When applied to a form via data-control="checkout-form", it:

  • Listens for changes to country, state, and address book selectors.
  • Automatically submits updated address data when the customer changes their country or selects an address book entry.
  • Updates the order summary and available shipping/payment methods without the customer clicking a button.
html
<form id="checkoutForm" data-control="checkout-form" data-request-flash>
    <!-- Checkout steps -->
</form>

This control is optional. The checkout works without it, but the customer would need to click "Continue" to see updated shipping rates after changing their address.

Events ​

The checkout process fires several events. See the Events documentation for details:

  • shop.beforePlaceOrder: Before the order is created.
  • shop.beforeCreateOrderRecord: Before a new order is saved to the database.
  • shop.beforeUpdateOrderRecord: Before an existing order is updated (amendment).
  • shop.newOrder: After a new order is created.
  • shop.checkout.beforeSetCouponCode: Before a coupon code is validated (allows modification).