Skip to content

Multi-step guest checkout

Multi-step checkout is a common pattern used in e-commerce websites to guide users through the checkout process.

Some of the benefits of using a multi-step checkout process include:

  • Enhanced user experience: Breaking down the checkout process into multiple steps can make it feel less overwhelming for customers. Each step focuses on a specific task, such as entering shipping information or payment details, which can simplify the process and reduce errors1.

  • Increased conversion rates: A streamlined, step-by-step process can help reduce cart abandonment rates. Customers are more likely to complete their purchase if the process is clear and straightforward.

  • Customization and flexibility: Adobe Commerce allows for customization of each step in the checkout process. This means you can tailor the experience to meet the specific needs of your customers, such as offering different payment or shipping options.

The Commerce boilerplate template does not include a multi-step checkout flow by default, but you can easily implement one using Adobe’s drop-in components.

Step-by-step

The following steps describe how to modify the commerce-checkout.js and commerce-checkout.css block files in the boilerplate template to implement a multi-step checkout flow.

This tutorial covers the guest checkout experience. If you want to implement a custom payment method, customer checkout, virtual cart, or any other feature, you’ll need to adapt the code accordingly. The following table shows the features covered in this tutorial:

FeatureCovered
Custom payment method
Customer checkout
Empty cart
Guest checkout
Virtual cart
Order confirmation

Define layout

Define the layout for the checkout using a contextual fragment and reference the DOM elements where the containers need to be rendered. Replace the block content with this fragment.

export default async function decorate(block) {
...
// Define the Layout for the Checkout
const fragment = document.createRange().createContextualFragment(`
<div class="checkout__wrapper">
<div class="checkout__heading"></div>
<div class="checkout__empty-cart"></div>
<div class="checkout__content">
<div class="checkout__main">
<div class="checkout__shipping">
<div class="checkout__shipping-title"></div>
<div class="checkout__login"></div>
<div class="checkout__shipping-form"></div>
<div class="checkout__continue-to-delivery"></div>
</div>
<div class="checkout__delivery">
<div class="checkout__delivery-title"></div>
<div class="checkout__delivery-methods"></div>
<div class="checkout__continue-to-payment"></div>
</div>
<div class="checkout__payment">
<div class="checkout__payment-title"></div>
<div class="checkout__bill-to-shipping"></div>
<div class="checkout__billing-form"></div>
<div class="checkout__payment-methods"></div>
<div class="checkout__place-order"></div>
</div>
</div>
<div class="checkout__aside">
<div class="checkout__order-summary"></div>
<div class="checkout__cart-summary"></div>
</div>
</div>
</div>
`);
const $heading = fragment.querySelector('.checkout__heading');
const $emptyCart = fragment.querySelector('.checkout__empty-cart');
const $content = fragment.querySelector('.checkout__content');
const $orderSummary = fragment.querySelector('.checkout__order-summary');
const $cartSummary = fragment.querySelector('.checkout__cart-summary');
const $shippingTitle = fragment.querySelector('.checkout__shipping-title');
const $login = fragment.querySelector('.checkout__login');
const $shippingForm = fragment.querySelector('.checkout__shipping-form');
const $continueToDeliveryBtn = fragment.querySelector(
'.checkout__continue-to-delivery',
);
const $deliveryTitle = fragment.querySelector('.checkout__delivery-title');
const $deliveryMethods = fragment.querySelector(
'.checkout__delivery-methods',
);
const $continueToPaymentBtn = fragment.querySelector(
'.checkout__continue-to-payment',
);
const $paymentTitle = fragment.querySelector('.checkout__payment-title');
const $billToShipping = fragment.querySelector('.checkout__bill-to-shipping');
const $billingForm = fragment.querySelector('.checkout__billing-form');
const $paymentMethods = fragment.querySelector('.checkout__payment-methods');
const $placeOrder = fragment.querySelector('.checkout__place-order');
block.replaceChildren(fragment);
};

Render initial containers

Render the initial containers in the corresponding DOM elements that you created in the previous step.

export default async function decorate(block) {
...
const [
heading,
_shippingInfoHeading,
__shippingMethodHeading,
_paymentHeading,
_loginForm,
shippingFormSkeleton,
continueToDeliveryBtn,
orderSummary,
_cartSummary,
] = await Promise.all([
UI.render(Header, {
title: 'Guest Checkout',
size: 'large',
divider: false,
})($heading),
UI.render(Header, {
title: '1. SHIPPING INFORMATION',
size: 'medium',
divider: false,
})($shippingTitle),
UI.render(Header, {
title: '2. SHIPPING METHOD',
size: 'medium',
divider: false,
})($deliveryTitle),
UI.render(Header, {
title: '3. PAYMENT INFORMATION',
size: 'medium',
divider: false,
})($paymentTitle),
// render the initial containers
CheckoutProvider.render(LoginForm, {
name: LOGIN_FORM_NAME,
})($login),
AccountProvider.render(AddressForm, {
isOpen: true,
showFormLoader: true,
})($shippingForm),
UI.render(Button, {
children: 'CONTINUE TO SHIPPING METHOD',
disabled: true,
onClick: async () => {
await continueToDelivery();
},
})($continueToDeliveryBtn),
CartProvider.render(OrderSummary)($orderSummary),
CartProvider.render(CartSummaryList, {
variant: 'secondary',
slots: {
Heading: (headingCtx) => {
const title = 'Your Cart ({count})';
const cartSummaryListHeading = document.createElement('div');
cartSummaryListHeading.classList.add('cart-summary-list__heading');
const cartSummaryListHeadingText = document.createElement('div');
cartSummaryListHeadingText.classList.add(
'cart-summary-list__heading-text',
);
cartSummaryListHeadingText.innerText = title.replace(
'({count})',
headingCtx.count ? `(${headingCtx.count})` : '',
);
const editCartLink = document.createElement('a');
editCartLink.classList.add('cart-summary-list__edit');
editCartLink.href = '/cart';
editCartLink.rel = 'noreferrer';
editCartLink.innerText = 'Edit';
cartSummaryListHeading.appendChild(cartSummaryListHeadingText);
cartSummaryListHeading.appendChild(editCartLink);
headingCtx.appendChild(cartSummaryListHeading);
headingCtx.onChange((nextHeadingCtx) => {
cartSummaryListHeadingText.innerText = title.replace(
'({count})',
nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '',
);
});
},
},
})($cartSummary),
]);
};

At this point, if you access the page where you are using the block, you should see that all containers are being rendered. The layout still needs some work, so let’s move on to the next step.

Render initial containers without styles

Render initial containers without styles

Add styles

Add some styles to the layout and containers to make them look good. Open the commerce-checkout.css file and add the following styles:

/* stylelint-disable selector-class-pattern */
.checkout__wrapper {
padding-left: 3rem;
padding-right: 3rem;
}
.checkout__banners {
padding-top: 1.5rem;
}
.checkout__content {
display: grid;
align-items: start;
grid-template-columns: repeat(var(--grid-4-columns), 1fr);
gap: var(--spacing-big);
padding-top: 1.5rem;
}
.checkout__content--empty {
display: none;
}
.checkout__empty-cart {
padding-top: 1.5rem;
}
.checkout__main {
display: grid;
grid-column: 1 / span 7;
row-gap: var(--spacing-xbig);
}
.checkout__aside {
display: grid;
grid-column: 9 / span 4;
row-gap: var(--spacing-xbig);
}
.checkout__shipping {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__shipping:has(> div:empty:not(.checkout__continue-to-delivery)) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
.checkout__delivery {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__delivery:has(> div:empty:not(.checkout__continue-to-payment)) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
.checkout__payment {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__payment:has(> div:empty) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
/* temporary fix to hide the default cart heading */
[data-testid='default-cart-heading'] {
display: none;
}
/* Responsive adjustments */
@media only screen and (width <= 768px) {
.checkout__wrapper {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.checkout__content {
grid-template-columns: 1fr;
gap: var(--spacing-big) 0;
}
.checkout__main,
.checkout__aside {
grid-column: auto;
}
.checkout__aside {
order: -1;
}
}

Render containers with styles

Render containers with styles

Display/remove empty cart

Check if the cart is empty and display a message to the user. To do this, create a new container called EmptyCart.

  1. Create two utility functions to display or remove the EmptyCart container when needed:

    let emptyCart;
    const displayEmptyCart = async () => {
    if (emptyCart) return;
    heading.setProps((prev) => ({
    ...prev,
    title: 'Empty Cart',
    }));
    emptyCart = await CartProvider.render(EmptyCart, {
    routeCTA: () => '/',
    })($emptyCart);
    $content.classList.add('checkout__content--empty');
    };
    const removeEmptyCart = () => {
    if (!emptyCart) return;
    emptyCart.remove();
    emptyCart = null;
    $emptyCart.innerHTML = '';
    heading.setProps((prev) => ({
    ...prev,
    title: 'Guest Checkout',
    }));
    $content.classList.remove('checkout__content--empty');
    };
  2. Add the following declarations at the end of the decorate function to start listening for the checkout/initialized and checkout/updated events to determine when to display the empty cart.

    events.on('checkout/initialized', handleCheckoutInitialized, { eager: true });
    events.on('checkout/updated', handleCheckoutUpdated);
  3. Add the missing handlers for the previous events.

    async function handleCheckoutInitialized(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    }
    async function handleCheckoutUpdated(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    removeEmptyCart();
    }

    At this point, if you access the checkout page without a cart, you should see the EmptyCart container displayed.

    Empty cart

    Empty cart

Handle shipping method form

  1. You’ve probably noticed that even after initializing the checkout the shipping address form still loads. To resolve this, create a new utility method called continueToShipping to remove the skeleton and render the shipping method form when the checkout is initialized.

    let shippingForm;
    const continueToShipping = async (initialData = null) => {
    if (shippingForm) return;
    // cleanup
    shippingFormSkeleton.remove();
    $shippingForm.innerHTML = '';
    shippingForm = await AccountProvider.render(AddressForm, {
    addressesFormTitle: 'Shipping address',
    className: 'checkout-shipping-form__address-form',
    formName: SHIPPING_FORM_NAME,
    hideActionFormButtons: true,
    inputsDefaultValueSet: initialData ?? {
    countryCode: storeConfig.defaultCountry,
    },
    isOpen: true,
    onChange: debounce((values) => {
    setAddressOnCart(values, checkoutApi.setShippingAddress);
    }, DEBOUNCE_TIME),
    showBillingCheckBox: false,
    showShippingCheckBox: false,
    })($shippingForm);
    };
  2. Update the handlers to use the new method.

    async function handleCheckoutInitialized(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    }
    async function handleCheckoutUpdated(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    removeEmptyCart();
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    }

Enable shipping

In the render initial containers step, we added a call to a non-existing continueToDelivery method and disabled the CONTINUE TO SHIPPING METHOD button by default. Now we are going to enable the button when the user selects a shipping address and implement the missing method.

  1. Update the handleCheckoutUpdated handler to enable the button when the received data contains a selected shipping address.

    async function handleCheckoutUpdated(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    removeEmptyCart();
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    if (!cartShippingAddress) return;
    continueToDeliveryBtn.setProps((prev) => ({
    ...prev,
    disabled: false,
    }));
    }
  2. Update the logic inside the handleCheckoutInitialized handler to automatically call the continueToDelivery method if the cart already contains the necessary data.

    async function handleCheckoutInitialized(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    // continue to delivery
    if (!cartShippingAddress) return;
    await continueToDelivery();
    }
  3. Create the continueToDelivery function to render all the containers that are relevant to the delivery step.

    let deliveryMethods;
    let continueToPaymentBtn;
    const continueToDelivery = async () => {
    if (deliveryMethods) return;
    deliveryMethods = await CheckoutProvider.render(ShippingMethods, {
    hideOnVirtualCart: true,
    onCheckoutDataUpdate: () => {
    cartApi.refreshCart().catch(console.error);
    },
    })($deliveryMethods);
    orderSummary.setProps((prev) => ({
    ...prev,
    slots: {
    EstimateShipping: (esCtx) => {
    const estimateShippingForm = document.createElement('div');
    CheckoutProvider.render(EstimateShipping)(estimateShippingForm);
    esCtx.appendChild(estimateShippingForm);
    },
    },
    }));
    continueToDeliveryBtn.remove();
    $continueToDeliveryBtn.remove();
    continueToPaymentBtn = await UI.render(Button, {
    children: 'CONTINUE TO PAYMENT INFORMATION',
    disabled: true,
    onClick: async () => {
    await continueToPayment();
    },
    })($continueToPaymentBtn);
    };

    Continue to shipping

    Continue to shipping

Enable payment

Similar to the enable shipping step, you now need to add the logic to enable the CONTINUE TO PAYMENT INFORMATION button and implement the missing continueToPayment method.

  1. Update the logic inside the handleCheckoutInitialized handler to automatically call the continueToPayment method if the cart already contains the necessary data.

    async function handleCheckoutInitialized(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    // continue to delivery
    if (!cartShippingAddress) return;
    await continueToDelivery();
    // continue to payment
    const deliveryMethod = getCartDeliveryMethod(data);
    if (!deliveryMethod) return;
    await continueToPayment();
    }
    async function handleCheckoutUpdated(data) {
    if (isEmptyCart(data)) {
    await displayEmptyCart();
    return;
    }
    removeEmptyCart();
    // continue to shipping
    const cartShippingAddress = getCartAddress(data, 'shipping');
    await continueToShipping(cartShippingAddress);
    if (!cartShippingAddress) return;
    continueToDeliveryBtn.setProps((prev) => ({
    ...prev,
    disabled: false,
    }));
    const deliveryMethod = getCartDeliveryMethod(data);
    if (!deliveryMethod) return;
    continueToPaymentBtn.setProps((prev) => ({
    ...prev,
    disabled: false,
    }));
    }
  2. Create the continueToPayment function to render all the containers that are relevant to the payment step.

    let billToShipping;
    let billingForm;
    let paymentMethods;
    let placeOrder;
    const continueToPayment = async () => {
    if (!billToShipping) {
    billToShipping = await CheckoutProvider.render(BillToShippingAddress, {
    hideOnVirtualCart: true,
    onChange: (checked) => {
    $billingForm.style.display = checked ? 'none' : 'block';
    },
    })($billToShipping);
    }
    if (!billingForm) {
    billingForm = await AccountProvider.render(AddressForm, {
    addressesFormTitle: 'Billing address',
    className: 'checkout-billing-form__address-form',
    formName: BILLING_FORM_NAME,
    hideActionFormButtons: true,
    isOpen: true,
    onChange: debounce((values) => {
    setAddressOnCart(values, checkoutApi.setBillingAddress);
    }, DEBOUNCE_TIME),
    showBillingCheckBox: false,
    showShippingCheckBox: false,
    })($billingForm);
    }
    if (!paymentMethods) {
    paymentMethods = await CheckoutProvider.render(PaymentMethods)($paymentMethods);
    }
    if (!placeOrder) {
    placeOrder = await CheckoutProvider.render(PlaceOrder)($placeOrder);
    }
    continueToPaymentBtn.remove();
    $continueToPaymentBtn.remove();
    };

    Continue to payment

    Continue to payment

At this point, you should have a fully functional multi-step checkout, but there’s still some work to do. Let’s move on to the last step.

Create order confirmation

The last step to complete the multi-step checkout process is to create an order confirmation page. To make things easy, you can reuse the code from the commerce-checkout.js block in the boilerplate template.

  1. Copy the displayOrderConfirmation function.

  2. Copy the handleCheckoutOrder handler.

  3. Register the checkout/order event listener.

    events.on('checkout/initialized', handleCheckoutInitialized, { eager: true });
    events.on('checkout/order', handleCheckoutOrder);
    events.on('checkout/updated', handleCheckoutUpdated);

    Order confirmation

    Order confirmation

Example

The following files show the complete JS and CSS code for the multi-step checkout flow:

commerce-checkout-multi-step.js
// Dropin Tools
import { events } from '@dropins/tools/event-bus.js';
import { initializers } from '@dropins/tools/initializer.js';
// Dropin Components
import { Button, Header, provider as UI } from '@dropins/tools/components.js';
// Auth Dropin
import SignUp from '@dropins/storefront-auth/containers/SignUp.js';
import { render as AuthProvider } from '@dropins/storefront-auth/render.js';
// Account Dropin
import AddressForm from '@dropins/storefront-account/containers/AddressForm.js';
import { render as AccountProvider } from '@dropins/storefront-account/render.js';
// Cart Dropin
import * as cartApi from '@dropins/storefront-cart/api.js';
import { CartSummaryList } from '@dropins/storefront-cart/containers/CartSummaryList.js';
import EmptyCart from '@dropins/storefront-cart/containers/EmptyCart.js';
import { OrderSummary } from '@dropins/storefront-cart/containers/OrderSummary.js';
import { render as CartProvider } from '@dropins/storefront-cart/render.js';
// Checkout Dropin
import * as checkoutApi from '@dropins/storefront-checkout/api.js';
import BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';
import EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';
import LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';
import OrderConfirmationHeader from '@dropins/storefront-checkout/containers/OrderConfirmationHeader.js';
import PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';
import PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';
import ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';
import { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';
// Order Dropin Modules
import * as orderApi from '@dropins/storefront-order/api.js';
import CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';
import OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';
import OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';
import OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';
import ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';
import { render as OrderProvider } from '@dropins/storefront-order/render.js';
import { getUserTokenCookie } from '../../scripts/initializers/index.js';
// Block-level
import {
getCartAddress,
getCartDeliveryMethod,
setAddressOnCart,
} from '../../scripts/checkout.js';
import createModal from '../modal/modal.js';
export default async function decorate(block) {
// Initializers
import('../../scripts/initializers/auth.js');
import('../../scripts/initializers/checkout.js');
const DEBOUNCE_TIME = 1000;
const LOGIN_FORM_NAME = 'login-form';
const SHIPPING_FORM_NAME = 'selectedShippingAddress';
const BILLING_FORM_NAME = 'selectedBillingAddress';
const SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;
const BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;
// Pre-fetch checkout store configuration
const storeConfig = await checkoutApi.getStoreConfig();
// Define the Layout for the Checkout
const fragment = document.createRange().createContextualFragment(`
<div class="checkout__wrapper">
<div class="checkout__heading"></div>
<div class="checkout__empty-cart"></div>
<div class="checkout__content">
<div class="checkout__main">
<div class="checkout__shipping">
<div class="checkout__shipping-title"></div>
<div class="checkout__login"></div>
<div class="checkout__shipping-form"></div>
<div class="checkout__continue-to-delivery"></div>
</div>
<div class="checkout__delivery">
<div class="checkout__delivery-title"></div>
<div class="checkout__delivery-methods"></div>
<div class="checkout__continue-to-payment"></div>
</div>
<div class="checkout__payment">
<div class="checkout__payment-title"></div>
<div class="checkout__bill-to-shipping"></div>
<div class="checkout__billing-form"></div>
<div class="checkout__payment-methods"></div>
<div class="checkout__place-order"></div>
</div>
</div>
<div class="checkout__aside">
<div class="checkout__order-summary"></div>
<div class="checkout__cart-summary"></div>
</div>
</div>
</div>
`);
const $heading = fragment.querySelector('.checkout__heading');
const $emptyCart = fragment.querySelector('.checkout__empty-cart');
const $content = fragment.querySelector('.checkout__content');
const $orderSummary = fragment.querySelector('.checkout__order-summary');
const $cartSummary = fragment.querySelector('.checkout__cart-summary');
const $shippingTitle = fragment.querySelector('.checkout__shipping-title');
const $login = fragment.querySelector('.checkout__login');
const $shippingForm = fragment.querySelector('.checkout__shipping-form');
const $continueToDeliveryBtn = fragment.querySelector(
'.checkout__continue-to-delivery',
);
const $deliveryTitle = fragment.querySelector('.checkout__delivery-title');
const $deliveryMethods = fragment.querySelector(
'.checkout__delivery-methods',
);
const $continueToPaymentBtn = fragment.querySelector(
'.checkout__continue-to-payment',
);
const $paymentTitle = fragment.querySelector('.checkout__payment-title');
const $billToShipping = fragment.querySelector('.checkout__bill-to-shipping');
const $billingForm = fragment.querySelector('.checkout__billing-form');
const $paymentMethods = fragment.querySelector('.checkout__payment-methods');
const $placeOrder = fragment.querySelector('.checkout__place-order');
block.replaceChildren(fragment);
// Render the initial containers
const [
heading,
_shippingInfoHeading,
__shippingMethodHeading,
_paymentHeading,
_loginForm,
shippingFormSkeleton,
continueToDeliveryBtn,
orderSummary,
_cartSummary,
] = await Promise.all([
UI.render(Header, {
title: 'Guest Checkout',
size: 'large',
divider: false,
})($heading),
UI.render(Header, {
title: '1. SHIPPING INFORMATION',
size: 'medium',
divider: false,
})($shippingTitle),
UI.render(Header, {
title: '2. SHIPPING METHOD',
size: 'medium',
divider: false,
})($deliveryTitle),
UI.render(Header, {
title: '3. PAYMENT INFORMATION',
size: 'medium',
divider: false,
})($paymentTitle),
// render the initial containers
CheckoutProvider.render(LoginForm, {
name: LOGIN_FORM_NAME,
})($login),
AccountProvider.render(AddressForm, {
isOpen: true,
showFormLoader: true,
})($shippingForm),
UI.render(Button, {
children: 'CONTINUE TO SHIPPING METHOD',
disabled: true,
onClick: async () => {
await continueToDelivery();
},
})($continueToDeliveryBtn),
CartProvider.render(OrderSummary)($orderSummary),
CartProvider.render(CartSummaryList, {
variant: 'secondary',
slots: {
Heading: (headingCtx) => {
const title = 'Your Cart ({count})';
const cartSummaryListHeading = document.createElement('div');
cartSummaryListHeading.classList.add('cart-summary-list__heading');
const cartSummaryListHeadingText = document.createElement('div');
cartSummaryListHeadingText.classList.add(
'cart-summary-list__heading-text',
);
cartSummaryListHeadingText.innerText = title.replace(
'({count})',
headingCtx.count ? `(${headingCtx.count})` : '',
);
const editCartLink = document.createElement('a');
editCartLink.classList.add('cart-summary-list__edit');
editCartLink.href = '/cart';
editCartLink.rel = 'noreferrer';
editCartLink.innerText = 'Edit';
cartSummaryListHeading.appendChild(cartSummaryListHeadingText);
cartSummaryListHeading.appendChild(editCartLink);
headingCtx.appendChild(cartSummaryListHeading);
headingCtx.onChange((nextHeadingCtx) => {
cartSummaryListHeadingText.innerText = title.replace(
'({count})',
nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '',
);
});
},
},
})($cartSummary),
]);
// Dynamic containers and components
let modal;
const showModal = async (content) => {
modal = await createModal([content]);
modal.showModal();
};
let emptyCart;
const displayEmptyCart = async () => {
if (emptyCart) return;
heading.setProps((prev) => ({
...prev,
title: 'Empty Cart',
}));
emptyCart = await CartProvider.render(EmptyCart, {
routeCTA: () => '/',
})($emptyCart);
$content.classList.add('checkout__content--empty');
};
const removeEmptyCart = () => {
if (!emptyCart) return;
emptyCart.remove();
emptyCart = null;
$emptyCart.innerHTML = '';
heading.setProps((prev) => ({
...prev,
title: 'Guest Checkout',
}));
$content.classList.remove('checkout__content--empty');
};
let shippingForm;
const continueToShipping = async (initialData = null) => {
if (shippingForm) return;
// cleanup
shippingFormSkeleton.remove();
$shippingForm.innerHTML = '';
shippingForm = await AccountProvider.render(AddressForm, {
addressesFormTitle: 'Shipping address',
className: 'checkout-shipping-form__address-form',
formName: SHIPPING_FORM_NAME,
hideActionFormButtons: true,
inputsDefaultValueSet: initialData ?? {
countryCode: storeConfig.defaultCountry,
},
isOpen: true,
onChange: setAddressOnCart({
api: checkoutApi.setShippingAddress,
debounceMs: DEBOUNCE_TIME,
placeOrderBtn: placeOrder,
}),
showBillingCheckBox: false,
showShippingCheckBox: false,
})($shippingForm);
};
let deliveryMethods;
let continueToPaymentBtn;
const continueToDelivery = async () => {
if (deliveryMethods) return;
deliveryMethods = await CheckoutProvider.render(ShippingMethods, {
hideOnVirtualCart: true,
onCheckoutDataUpdate: () => {
cartApi.refreshCart().catch(console.error);
},
})($deliveryMethods);
orderSummary.setProps((prev) => ({
...prev,
slots: {
EstimateShipping: (esCtx) => {
const estimateShippingForm = document.createElement('div');
CheckoutProvider.render(EstimateShipping)(estimateShippingForm);
esCtx.appendChild(estimateShippingForm);
},
},
}));
continueToDeliveryBtn.remove();
$continueToDeliveryBtn.remove();
continueToPaymentBtn = await UI.render(Button, {
children: 'CONTINUE TO PAYMENT INFORMATION',
disabled: true,
onClick: async () => {
await continueToPayment();
},
})($continueToPaymentBtn);
};
let billToShipping;
let billingForm;
let paymentMethods;
let placeOrder;
const continueToPayment = async () => {
if (!billToShipping) {
billToShipping = await CheckoutProvider.render(BillToShippingAddress, {
hideOnVirtualCart: true,
onChange: (checked) => {
$billingForm.style.display = checked ? 'none' : 'block';
},
})($billToShipping);
}
if (!billingForm) {
billingForm = await AccountProvider.render(AddressForm, {
addressesFormTitle: 'Billing address',
className: 'checkout-billing-form__address-form',
formName: BILLING_FORM_NAME,
hideActionFormButtons: true,
isOpen: true,
onChange: setAddressOnCart({
api: checkoutApi.setBillingAddress,
debounceMs: DEBOUNCE_TIME,
placeOrderBtn: placeOrder,
}),
showBillingCheckBox: false,
showShippingCheckBox: false,
})($billingForm);
}
if (!paymentMethods) {
paymentMethods = await CheckoutProvider.render(PaymentMethods)($paymentMethods);
}
if (!placeOrder) {
placeOrder = await CheckoutProvider.render(PlaceOrder)($placeOrder);
}
continueToPaymentBtn.remove();
$continueToPaymentBtn.remove();
};
const displayOrderConfirmation = async (orderData) => {
// Define the Layout for the Order Confirmation
const orderConfirmationFragment = document.createRange()
.createContextualFragment(`
<div class="order-confirmation">
<div class="order-confirmation__main">
<div class="order-confirmation__block order-confirmation__header"></div>
<div class="order-confirmation__block order-confirmation__order-status"></div>
<div class="order-confirmation__block order-confirmation__shipping-status"></div>
<div class="order-confirmation__block order-confirmation__customer-details"></div>
</div>
<div class="order-confirmation__aside">
<div class="order-confirmation__block order-confirmation__order-cost-summary"></div>
<div class="order-confirmation__block order-confirmation__order-product-list"></div>
<div class="order-confirmation__block order-confirmation__footer"></div>
</div>
</div>
`);
// Order confirmation elements
const $orderConfirmationHeader = orderConfirmationFragment.querySelector(
'.order-confirmation__header',
);
const $orderStatus = orderConfirmationFragment.querySelector(
'.order-confirmation__order-status',
);
const $shippingStatus = orderConfirmationFragment.querySelector(
'.order-confirmation__shipping-status',
);
const $customerDetails = orderConfirmationFragment.querySelector(
'.order-confirmation__customer-details',
);
const $orderCostSummary = orderConfirmationFragment.querySelector(
'.order-confirmation__order-cost-summary',
);
const $orderProductList = orderConfirmationFragment.querySelector(
'.order-confirmation__order-product-list',
);
const $orderConfirmationFooter = orderConfirmationFragment.querySelector(
'.order-confirmation__footer',
);
await initializers.mountImmediately(orderApi.initialize, { orderData });
block.replaceChildren(orderConfirmationFragment);
const onSignUpClick = async ({ inputsDefaultValueSet, addressesData }) => {
const signUpForm = document.createElement('div');
AuthProvider.render(SignUp, {
routeSignIn: () => '/customer/login',
routeRedirectOnEmailConfirmationClose: () => '/customer/account',
inputsDefaultValueSet,
addressesData,
})(signUpForm);
showModal(signUpForm);
};
CheckoutProvider.render(OrderConfirmationHeader, {
orderData,
onSignUpClick,
})($orderConfirmationHeader);
OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(
$orderStatus,
);
OrderProvider.render(ShippingStatus)($shippingStatus);
OrderProvider.render(CustomerDetails)($customerDetails);
OrderProvider.render(OrderCostSummary)($orderCostSummary);
OrderProvider.render(OrderProductList)($orderProductList);
$orderConfirmationFooter.innerHTML = `
<div class="order-confirmation-footer__continue-button"></div>
<div class="order-confirmation-footer__contact-support">
<p>
Need help?
<a
href="/support"
rel="noreferrer"
class="order-confirmation-footer__contact-support-link"
data-testid="order-confirmation-footer__contact-support-link"
>
Contact us
</a>
</p>
</div>
`;
const $orderConfirmationFooterContinueBtn = $orderConfirmationFooter.querySelector(
'.order-confirmation-footer__continue-button',
);
UI.render(Button, {
children: 'Continue shopping',
'data-testid': 'order-confirmation-footer__continue-button',
className: 'order-confirmation-footer__continue-button',
size: 'medium',
variant: 'primary',
type: 'submit',
href: '/',
})($orderConfirmationFooterContinueBtn);
};
// Define checkout event handlers and shared utilities
const isEmptyCart = (data) => data === null || data.isEmpty;
async function handleCheckoutInitialized(data) {
if (isEmptyCart(data)) {
await displayEmptyCart();
return;
}
// continue to shipping
const cartShippingAddress = getCartAddress(data, 'shipping');
await continueToShipping(cartShippingAddress);
// continue to delivery
if (!cartShippingAddress) return;
await continueToDelivery();
// continue to payment
const deliveryMethod = getCartDeliveryMethod(data);
if (!deliveryMethod) return;
await continueToPayment();
}
async function handleCheckoutUpdated(data) {
if (isEmptyCart(data)) {
await displayEmptyCart();
return;
}
removeEmptyCart();
// continue to shipping
const cartShippingAddress = getCartAddress(data, 'shipping');
await continueToShipping(cartShippingAddress);
if (!cartShippingAddress) return;
continueToDeliveryBtn.setProps((prev) => ({
...prev,
disabled: false,
}));
const deliveryMethod = getCartDeliveryMethod(data);
if (!deliveryMethod) return;
continueToPaymentBtn.setProps((prev) => ({
...prev,
disabled: false,
}));
}
async function handleCheckoutOrder(orderData) {
// clear address form data
sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);
sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);
const token = getUserTokenCookie();
const orderRef = token ? orderData.number : orderData.token;
const encodedOrderRef = encodeURIComponent(orderRef);
window.history.pushState(
{},
'',
`/order-details?orderRef=${encodedOrderRef}`,
);
// TODO cleanup checkout containers
await displayOrderConfirmation(orderData);
}
events.on('checkout/initialized', handleCheckoutInitialized, { eager: true });
events.on('checkout/order', handleCheckoutOrder);
events.on('checkout/updated', handleCheckoutUpdated);
}
commerce-checkout-multi-step.css
.checkout__wrapper {
padding-left: 3rem;
padding-right: 3rem;
}
.checkout__banners {
padding-top: 1.5rem;
}
.checkout__content {
display: grid;
align-items: start;
grid-template-columns: repeat(var(--grid-4-columns), 1fr);
gap: var(--spacing-big);
padding-top: 1.5rem;
}
.checkout__content--empty {
display: none;
}
.checkout__empty-cart {
padding-top: 1.5rem;
}
.checkout__main {
display: grid;
grid-column: 1 / span 7;
row-gap: var(--spacing-xbig);
}
.checkout__aside {
display: grid;
grid-column: 9 / span 4;
row-gap: var(--spacing-xbig);
}
.checkout__shipping {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__shipping:has(> div:empty:not(.checkout__continue-to-delivery)) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
.checkout__delivery {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__delivery:has(> div:empty:not(.checkout__continue-to-payment)) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
.checkout__payment {
display: flex;
flex-direction: column;
gap: var(--spacing-big);
padding: 0;
}
.checkout__payment:has(> div:empty) {
gap: 0;
padding-top: var(--spacing-small);
padding-bottom: var(--spacing-small);
border-top: var(--shape-border-width-3) solid var(--color-neutral-400);
border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);
}
/* temporary fix to hide the default cart heading */
[data-testid='default-cart-heading'] {
display: none;
}
/* Responsive adjustments */
@media only screen and (width <= 768px) {
.checkout__wrapper {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.checkout__content {
grid-template-columns: 1fr;
gap: var(--spacing-big) 0;
}
.checkout__main,
.checkout__aside {
grid-column: auto;
}
.checkout__aside {
order: -1;
}
}