Adding Checkout Steps in Magento 2

I thought I'd cover adding a checkout step in Magento 2 since I wrote a guide for Magento 1, even though there is plenty of documentation on how to do so online.

It turns out that adding a step on Magento 2 is much easier than Magento. For a start, you do not have to extend and override core Magento and JavaScript files to the extent that you have to with Magento 1.

The Magento 2 core developers seem to have recognised the need for adding in additional steps to the checkout, and with a combination of layout XML, JavaScript and Knockout, this can be achieved with relative ease.

The code I show below will work as of Magento Open Source version 2.2, however if you have difficulty implementing any of the code examples, you are more than welcome to get in touch.

As with any customisations, create a custom module that will contain your code.

file: app/code/SussexDev/CheckoutStep/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'SussexDev_CheckoutStep',
    __DIR__
);
file: app/code/SussexDev/CheckoutStep/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="SussexDev_CheckoutStep" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Checkout" />
        </sequence>
    </module>
</config>

Enable the module and run the database upgrade command.

$ /path/to/your/php bin/magento module:enable SussexDev_CustomStep
$ /path/to/your/php bin/magento setup:upgrade

Magento's default checkout steps are defined in the checkout_index_index.xml layout file of the Magento_Checkout module via components.

Create a checkout_index_index.xml file within the custom module and add a checkout step component. This is where the 'unique identifier' for your step will also be defined.

In my example, I use customstep.

file: app/code/SussexDev/CheckoutStep/view/frontend/layout/checkout_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">

                                        <!-- The new step you add -->
                                        <item name="customstep" xsi:type="array">
                                            <item name="component" xsi:type="string">SussexDev_CheckoutStep/js/view/customstep</item>
                                            <item name="sortOrder" xsi:type="string">0</item>
                                            <item name="children" xsi:type="array">
                                                <!-- Add any child components for your step here -->
                                            </item>
                                        </item>

                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

It should be noted that in order to display the checkout step before the default shipping step, the sortOrder value should be less than 1.

To display the custom step between the shipping step and payment step, the sortOrder value should be greater than 1 but less than or equal to 2. Using 2 seems to work even though Magento's DevDocs do not state this. If you try and use the decimal value, your custom step will not show at all.

If the step needs to be displayed after the default payment step, set the sortOrder value to be greater than 2.

For now, be adding the step at the beginning of the checkout steps.

Next, add the customstep.js JavaScript file. This file should be added within the module's view/frontend/web/js/view directory.

file: app/code/SussexDev/CheckoutStep/view/frontend/web/js/view/customstep.js
define(
    [
        'ko',
        'uiComponent',
        'underscore',
        'Magento_Checkout/js/model/step-navigator'
    ],
    function (
        ko,
        Component,
        _,
        stepNavigator
    ) {
        'use strict';

        return Component.extend({
            defaults: {
                template: 'SussexDev_CheckoutStep/customstep'
            },

            // Add here your logic to display step,
            isVisible: ko.observable(true),

            /**
             *
             * @returns {*}
             */
            initialize: function () {
                this._super();
                // Register your step
                stepNavigator.registerStep(
                    // Step code will be used as step content id in the component template
                    'customstep',
                    // Step alias
                    null,
                    // Step title value
                    'Custom Step',
                    // Observable property with logic when display step or hide step
                    this.isVisible,

                    _.bind(this.navigate, this),

                    /**
                     * Sort order value
                     * 'sort order value' < 10: step displays before shipping step;
                     * 10 < 'sort order value' < 20 : step displays between shipping and payment step
                     * 'sort order value' > 20 : step displays after payment step
                     */
                    1
                );

                return this;
            },

            /**
             * The navigate() method is responsible for navigation between checkout step
             * during checkout. You can add custom logic, for example some conditions
             * for switching to your custom step
             */
            navigate: function () {

            },

            /**
             * @returns void
             */
            navigateToNextStep: function () {
                stepNavigator.next();
            }
        });
    }
);

Within this file, the isVisible property plays an important role. If the custom checkout step displays before the other steps, then the property can be set to true. However, if the step is set to be the middle or last step, then isVisible should be set to false.

Now add the Knockout template that will render some content to show on the checkout step.

This example will just show the step with a Continue button to continue onto the next step.

file: app/code/SussexDev/CheckoutStep/view/frontend/web/template/customstep.html
<li id="customstep" data-bind="fadeVisible: isVisible">
    <div class="step-title" data-bind="i18n: 'Custom Step Title'" data-role="title"></div>
    <div id="checkout-step-title
         class="step-content"
         data-role="content">

        <form data-bind="submit: navigateToNextStep" novalidate="novalidate">
            <div class="actions-toolbar">
                <div class="primary">
                    <button data-role="opc-continue" type="submit" class="button action continue primary">
                        <span><!-- ko i18n: 'Next'--><!-- /ko --></span>
                    </button>
                </div>
            </div>
        </form>
    </div>
</li>

As the Magento DevDocs mention, the default shipping step's isVisible property is set to true by default. Therefore, if you are adding a custom step before the shipping step, you might run into an issue where both steps are active at the same time.

To resolve this, define a mixin that overrides the isVisible property. This can be done by adding a requirejs-config.js file within the custom module.
file: app/code/SussexDev/CheckoutStep/view/base/requirejs-config.js
var config = {
    'config': {
        'mixins': {
            'Magento_Checkout/js/view/shipping': {
                'SussexDev_CheckoutStep/js/view/shipping-payment-mixin': true
            },
            'Magento_Checkout/js/view/payment': {
                'SussexDev_CheckoutStep/js/view/shipping-payment-mixin': true
            }
        }
    }
}

Now create the shipping-payment-mixin.js file, and override the property.

file: app/code/SussexDev/CheckoutStep/view/frontend/web/js/view/shipping-payment-mixin.js
define(
    [
        'ko'
    ], function (ko) {
        'use strict';

        var mixin = {
            initialize: function () {
                this.visible = ko.observable(false);
                this._super();

                return this;
            }
        };

        return function (target) {
            return target.extend(mixin);
        };
    }
);

After regenerating static content and refreshing the cache, you should see the custom checkout step added at the beginning of the steps using the code above.

Should you wish to add the custom step in between the shipping and payment steps, then you don't need to create the mixin override.

Simply set the isVisible property of the custom checkout step to false, and change the sort order value within customstep.js to an integer between 10 and 20.

And lastly, change the sortOrder value within your checkout_index_index.xml file to an integer, such as 2.

Admittedly, this is a basic example of adding a custom step and doesn't contain any useful functionality that you might wish to use on the checkout, such as customers having to fill out some form data, select a delivery date etc.

I may write about this in future, but for now, I hope you enjoy what's here.