Introduction

How did I decide to write a blog post about the New Checkout Step in Magento 2? Recently, when doing some checkout development, I needed to obtain information about a recently placed order. The particular value I wanted to get was the order ID. My first idea was to create a controller that would return the required information right after the AJAX call made upon the purchase finalization. Sounds easy enough, right? Actually, not really. The main problem with this solution is the necessity of making an AJAX call flagged async: false. This is because we need to wait for it to execute before we can obtain the information we need. Also, we would have to write a lot of extra code. It is an avoidable inconvenience because a much better mechanism exists and it’s made specifically for tasks like this. Meet sections. 

What is a section?

Sections can be described as a data set that goes into the browser’s local storage after being loaded with an AJAX (POST) call. Specifically, you will find them under the mage-cache-storage key. This key is a JSON array which means that we can refer to a particular section by its key (like messages). Whenever a given section changes, Magento will load updated data. If you’d like to learn more about private content in Magento, check our other article

What exactly are we trying to do? We will make a completely new checkout step. Instead of a regular success page after placing an order, we will display basic order information – ID. 

Let’s start with the basics and create a polygon!

First, create a module to put our code in. Let’s call it Magently_Sections. Remember about loading the Magento_Checkout plugin in the sequence since we’ll be modifying it.

// file 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="Magently_Sections" setup_version="0.1.0">
        <sequence>
            <module name="Magento_Checkout"/>
        </sequence>
    </module>
</config>

// file registration.php
<?php

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Magently_Sections',
    __DIR__
);

The code above should be self-explanatory. Now simply execute bin/magento setup:upgrade and our new module is ready. 

New checkout step in Magento 2

To display extra information after placing an order requires creating a new checkout step. Let’s create a component:

// file view/frontend/web/js/view/resume.js
define([
    'ko',
    'uiComponent',
    'underscore',
    'Magento_Checkout/js/model/step-navigator'
], function (
    ko,
    Component,
    _,
    stepNavigator
) {
    return Component.extend({
        defaults: {
            template: 'Magently_Sections/resume',
            isVisible: ko.observable(false),
            stepCode: 'resume',
            stepTitle: 'Resume',
        },

        /**
         * @return {*}
         */
        initialize: function () {
            this._super();

            stepNavigator.registerStep(
                this.stepCode, null, this.stepTitle, this.isVisible, _.bind(this.navigate, this), 25
            );

            return this;
        },

        /**
         * @returns void
         */
        navigate: function () {
            this.isVisible(true);
        }
    });
});

We register the step and implement the base navigator functionality: going to a step. The proper step registration requires us to input the step code, alias (in our case it’s null), title, initial visibility value, navigating function for the created step (bound with underscore) and the progress bar position value. A value above 20 will make this step the last step, which is exactly what we need. Thanks to registering the step, the stepNavigator module will be able to manage it and react properly to interactions with it (like moving to this step). After creating a component, we need to create a template that we declared in the template variable:

// file view/frontend/web/template/resume.html
<li id="resume"
    class="checkout-resume"
    data-bind="fadeVisible: isVisible, attr: { id: stepCode }"
    role="presentation">
    <!-- here goes our template code-->
</li>

So far, we don’t have any data to display. The important thing is adding the ID attribute with the stepCode variable value so that the step is properly processed by the progress bar. The last thing to do is adding config to the layout file that will enable Magento to create the step. Let’s take a look at the code chunk below. 

// file view/frontend/layout/checkout_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <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">
                                            <item name="resume-step" xsi:type="array">
                                                <item name="component"
                                                      xsi:type="string">Magently_Sections/js/view/resume</item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </argument>
                </arguments>
            </referenceBlock>
        </referenceContainer>
    </body>
</page>

As you can see, this chunk isn’t very complex despite its appearance. It’s not half as bad as it may look. The whole magic happens in the jsLayout argument that is a simple array where we put our new elements. The record above translates to something like this: 

$jsLayout['components']['checkout']['children']['steps']['children']['resume-step'] = [

    'component' => 'Magently_Sections/js/view/resume'

];

This may bring associations with LayoutProcessor class plugins – and rightfully so. This whole thing we did in an XML file could be handled by a plugin just as well, but don’t we all love to hang out with XMLs? Let’s just add a new step named resume-step and set a component responsible for it. It should look something like this:

Modifying default behavior after placing an order

By default, Magento redirects us to the success page after placing the order. We would like to change it and display other data without redirection. To do that, we need to modify the default Magento functionality and simply trigger moving to the next step instead. This in turn can be done by doing a mixin of the default.js file. 

// file view/frontend/web/js/view/payment/default-mixin.js
define([
    'ko',
    'jquery',
    'Magento_Checkout/js/model/step-navigator'
], function (
    ko,
    $,
    stepNavigator
) {
    var mixin = {
        redirectAfterPlaceOrder: false,

        /**
         * After place order callback
         */
        afterPlaceOrder: function () {
            stepNavigator.next();
        },
    };

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

Turning off the redirection happens after setting the redirectAfterPlaceOrder variable to false. To move to the next step, we will use the afterPlaceOrder variable which contains the entire logic executed after placing an order. We also need to tell Magento that we created a mixin and it should be using it. Let’s create a file called requirejs-config.js:

// file view/frontend/requirejs-config.js
var config = {
    config: {
        'mixins': {
            'Magento_Checkout/js/view/payment/default': {
                'Magently_Sections/js/view/payment/default-mixin': true
            }
        }
    }
};

Let’s see if it works correctly.

Use sections Magento 2

Let’s create a section!

First, we will need to create a class that will process our section:

// file CustomerData/Order/LastPlaced.php
<?php

namespace Magently\Sections\CustomerData\Order;

use Magento\Checkout\Model\Session;
use Magento\Customer\CustomerData\SectionSourceInterface;

/**
 * LastPlaced source
 */
class LastPlaced implements SectionSourceInterface
{
    /**
     * @var Session $session
     */
    private $session;

    /**
     * @param Session $session
     */
    public function __construct(Session $session)
    {
        $this->session = $session;
    }

    /**
     * @return array
     */
    public function getSectionData()
    {
        $order = $this->session->getLastRealOrder();

        return [
            'increment_id' => $order->getIncrementId()
        ];
    }
}

The class has to contain a getSectionData method that will return an array of obtained data that we will be able to operate. It’s a result of implementing SectionSourceInterface. To obtain a recently placed order, we’ll use a session for the checkout. There is a method called getLastRealOrder that will let us request the most recently placed order. Then we pick the data we need and we return them in an array. The next step is to create a new section with di.xml. Let’s take a look at the code chunk below. 

// file etc/frontend/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magently\Sections\CustomerData\Order\LastPlaced">
        <arguments>
            <argument name="session" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument>
        </arguments>
    </type>
    <type name="Magento\Customer\CustomerData\SectionPoolInterface">
        <arguments>
            <argument name="sectionSourceMap" xsi:type="array">
                <item name="last-placed-order"
                      xsi:type="string">Magently\Sections\CustomerData\Order\LastPlaced</item>
            </argument>
        </arguments>
    </type>
</config>

Let’s not forget the optimization. We can use the session proxy. Thanks to this, the session class won’t be loaded until our method is actually called. We’ll provide the argument name that we’ll be using later (last-placed-order in this case) to the section declaration and the namespace to the class as its value. 

To avoid problems with not reloading sections, it’s a good practice to maintain the naming convention using dashes “-” instead e.g. camelCase. 

Now, in the sections.xml file, we have to define the action that will cause reloading of the created section. It’s done like this:

// file etc/frontend/sections.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
    <action name="rest/*/V1/carts/*/payment-information">
        <section name="last-placed-order"/>
    </action>
    <action name="rest/*/V1/guest-carts/*/payment-information">
        <section name="last-placed-order"/>
    </action>
</config>

We defined reloading a section in two actions. If you look at the Magento REST API Reference, you will learn that they are paths to two checkout endpoints – for non-registered buyers (guest-carts) and for the registered ones (carts). After executing bin/magento cache:flush and going through the purchase process, you won’t see any difference in comparison to what we’ve seen on the previous screen. Let’s change it.

Checkout step in Magento 2: Bringing life to the summary

Let’s go back to the JavaScript file that processes the step. We need to add the support of computed observable to automatically update the observable orderId value on section reloading. 

// file view/frontend/web/js/view/resume.js
define([
    ...,
    'Magento_Customer/js/customer-data'
], function (
    ...,
    customerData
) {
    return Component.extend({
        defaults: {
            ...
        },

        initialize: ...,

        /**
         * @inheritdoc
         */
        initObservable: function () {
            var order = null;

            this._super();
            this.orderId = ko.computed(function () {
                order = customerData.get('last-placed-order');

                return '#' + order().increment_id;
            }, this);

            return this;
        },

        navigate: ...
    });
});

The section content is fetched with the customerData module that we first have to add to define arguments. The key we will use to fetch the data is the name given when creating the section in di.xml. The last thing to do is to display the fetched value in the summary. Let’s take a look at the HTML template.

// file view/frontend/web/template/resume.html
<li ...>
    <span data-bind="i18n: 'Thanks for buying our product! Here is your order ID:'"></span>
    <span data-bind="text: orderId"></span>
</li>

We only added the thank you message and we bind the variable value as text. Let’s see the final effect of the section in action below:

Use Sections Magento 2

Voilà!

As you could see, sections are a very useful tool. Instead of creating useless controllers processing AJAX calls, we can just use this native Magento solution. It’s a much cleaner and coherent approach. Therefore, I hope that this article helps you cope with checkout step in Magento 2 and clear any doubts you had.