Back to Top

Add Billing Address Step on Checkout Magento2

Updated 23 February 2024

Here we will learn, How to Add Billing Address Step on Magento2 Checkout page

1. Create new file Webkul/CheckoutCustomization/view/frontend/web/js/view/billing-address-step.js

define(
    [
        'ko',
        'uiComponent',
        'underscore',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/step-navigator',
        'mage/translate'
    ],
    function (
        ko,
        Component,
        _,
        quote,
        stepNavigator,
        $t
    ) {
        'use strict';

        return Component.extend({
            defaults: {
                template: 'Webkul_CheckoutCustomization/view/billing-step'
            },

            //add here your logic to display step,
            isVisible: ko.observable(quote.isVirtual()),
            isVirtual: quote.isVirtual(),

            /**
             *
             * @returns {*}
             */
            initialize: function () {
                this._super();
                if (!quote.isVirtual()) { //update condition if you need to enable for virtual products
                // register your step
                stepNavigator.registerStep(
                    'custom-billing-step',
                    'custom-billing-step',
                    $t('Billing Address'),
                    this.isVisible, _.bind(this.navigate, this),
                    11
                );
                }

            },

            /**
             * 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();
            }
        });
    }
);

2. Now we need to create html template file (billing-step.html) defined in above file.
File: Webkul/CheckoutCustomization/view/frontend/web/template/billing-step.html

<!--The 'step_code' value from the .js file should be used-->
<li id="custom-billing-step" data-bind="fadeVisible: isVisible">
    <div class="step-title" data-bind="i18n: 'Billing Information'" data-role="title"></div>
    <div id="checkout-step-title" class="step-content" data-role="content">
        <form data-bind="submit: navigateToNextStep" novalidate="novalidate">
            <!-- ko foreach: getRegion('afterMethods') -->
            <!-- ko template: getTemplate() -->
            <!-- /ko -->
            <!-- /ko -->
            <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>

3. Create Webkul/CheckoutCustomization/view/frontend/layout/checkout_index_index.xml

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <css src="Webkul_CheckoutCustomization::css/style.css"/>
    </head>
    <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="custom-billing-step" xsi:type="array">
                                                <item name="component" xsi:type="string">uiComponent</item>
                                                <item name="sortOrder" xsi:type="string">2</item>
                                                <item name="children" xsi:type="array">
                                                    <item name="custom-billing" xsi:type="array">
                                                        <item name="component" xsi:type="string">Webkul_CheckoutCustomization/js/view/billing-address-step</item>
                                                        <item name="config" xsi:type="array">
                                                            <item name="title" xsi:type="string" translate="true">Billing Address</item>
                                                        </item>
                                                        <item name="children" xsi:type="array">
                                                            <item name="customer-email" xsi:type="array">
                                                                <item name="component" xsi:type="string">Magento_Checkout/js/view/form/element/email</item>
                                                                <item name="displayArea" xsi:type="string">customer-email</item>
                                                                <item name="tooltip" xsi:type="array">
                                                                    <item name="description" xsi:type="string" translate="true">We'll send your order confirmation here.</item>
                                                                </item>
                                                                <item name="children" xsi:type="array">
                                                                    <item name="before-login-form" xsi:type="array">
                                                                        <item name="component" xsi:type="string">uiComponent</item>
                                                                        <item name="displayArea" xsi:type="string">before-login-form</item>
                                                                        <item name="children" xsi:type="array">
                                                                            <!-- before login form fields -->
                                                                        </item>
                                                                    </item>
                                                                    <item name="additional-login-form-fields" xsi:type="array">
                                                                        <item name="component" xsi:type="string">uiComponent</item>
                                                                        <item name="displayArea" xsi:type="string">additional-login-form-fields</item>
                                                                        <item name="children" xsi:type="array">
                                                                            <!-- additional login form fields -->
                                                                        </item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                            <!-- merge your payment methods here -->
                                                            <item name="afterMethods" xsi:type="array">
                                                                <item name="component" xsi:type="string">uiComponent</item>
                                                                <item name="displayArea" xsi:type="string">afterMethods</item>
                                                                <item name="children" xsi:type="array">
                                                                    
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </argument>
                </arguments>
        </referenceBlock>
    </body>
</page>

Now, add css file: Webkul/CheckoutCustomization/view/frontend/web/css/style.css

@media (min-width: 768px), print {
    #custom-billing-step .actions-toolbar .primary {
        float: right;
        margin: 0;
    }
}
#custom-billing-step > .actions-toolbar > .primary .action.primary{
    line-height: 2.2rem;
    padding: 14px 17px;
    font-size: 1.8rem;
}
@media (min-width: 768px), print{
    #custom-billing-step .actions-toolbar .action-update {
        margin: 6px 20px 0 0;
    }
}
#custom-billing-step .checkout-billing-address{
    margin-bottom: 10px;
}


4. Create Webkul/CheckoutCustomization/Block/Checkout/LayoutProcessor.php to insert Billing Address form in the xml.

Searching for an experienced
Magento 2 Company ?
Find out More
<?php

namespace Webkul\CheckoutCustomization\Block\Checkout;

use Magento\Checkout\Helper\Data;
use Magento\Framework\App\ObjectManager;

class LayoutProcessor
{
    /**
     * @var \Magento\Customer\Model\AttributeMetadataDataProvider
     */
    private $attributeMetadataDataProvider;

    /**
     * @var \Magento\Ui\Component\Form\AttributeMapper
     */
    protected $attributeMapper;

    /**
     * @var AttributeMerger
     */
    protected $merger;

    /**
     * @var \Magento\Customer\Model\Options
     */
    private $options;

    /**
     * @var Data
     */
    private $checkoutDataHelper;

    /**
     * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider
     * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper
     * @param AttributeMerger $merger
     */
    public function __construct(
        \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider,
        \Magento\Ui\Component\Form\AttributeMapper $attributeMapper,
        \Magento\Checkout\Block\Checkout\AttributeMerger $merger
    ) {
        $this->attributeMetadataDataProvider = $attributeMetadataDataProvider;
        $this->attributeMapper = $attributeMapper;
        $this->merger = $merger;
    }

    /**
     * @deprecated
     * @return \Magento\Customer\Model\Options
     */
    private function getOptions()
    {
        if (!is_object($this->options)) {
            $this->options = ObjectManager::getInstance()->get(\Magento\Customer\Model\Options::class);
        }
        return $this->options;
    }

    /**
     * @return array
     */
    private function getAddressAttributes()
    {
        /** @var \Magento\Eav\Api\Data\AttributeInterface[] $attributes */
        $attributes = $this->attributeMetadataDataProvider->loadAttributesCollection(
            'customer_address',
            'customer_register_address'
        );

        $elements = [];
        foreach ($attributes as $attribute) {
            $code = $attribute->getAttributeCode();
            if ($attribute->getIsUserDefined()) {
                continue;
            }
            $elements[$code] = $this->attributeMapper->map($attribute);
            if (isset($elements[$code]['label'])) {
                $label = $elements[$code]['label'];
                $elements[$code]['label'] = __($label);
            }
        }
        return $elements;
    }

    /**
     * Convert elements(like prefix and suffix) from inputs to selects when necessary
     *
     * @param array $elements address attributes
     * @param array $attributesToConvert fields and their callbacks
     * @return array
     */
    private function convertElementsToSelect($elements, $attributesToConvert)
    {
        $codes = array_keys($attributesToConvert);
        foreach (array_keys($elements) as $code) {
            if (!in_array($code, $codes)) {
                continue;
            }
            $options = call_user_func($attributesToConvert[$code]);
            if (!is_array($options)) {
                continue;
            }
            $elements[$code]['dataType'] = 'select';
            $elements[$code]['formElement'] = 'select';

            foreach ($options as $key => $value) {
                $elements[$code]['options'][] = [
                    'value' => $key,
                    'label' => $value,
                ];
            }
        }

        return $elements;
    }

    /**
     * Process js Layout of block
     *
     * @param array $jsLayout
     * @return array
     */
    public function process($jsLayout)
    {
        $attributesToConvert = [
            'prefix' => [$this->getOptions(), 'getNamePrefixOptions'],
            'suffix' => [$this->getOptions(), 'getNameSuffixOptions'],
        ];

        $elements = $this->getAddressAttributes();
        $elements = $this->convertElementsToSelect($elements, $attributesToConvert);
        // The following code is a workaround for custom address attributes
        if (isset($jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
            ['payment']['children']
        )) {
            $jsLayout['components']['checkout']['children']['steps']['children']['custom-billing-step']['children']
            ['custom-billing']['children'] = $this->processNewStepsChildrenComponents(
                $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
                ['payment']['children'],
                $jsLayout['components']['checkout']['children']['steps']['children']['custom-billing-step']['children']
                ['custom-billing']['children'],
                $elements
            );

        }
        return $jsLayout;
    }

    /**
     * Appends billing address form component to payment layout
     *
     * @param array $paymentLayout
     * @param array $elements
     *
     * @return array
     */
    private function processNewStepsChildrenComponents(
        array $paymentLayout,
        array $newStepsLayout,
        array $elements
    ) {
        if (!isset($paymentLayout['payments-list']['children'])) {
            $paymentLayout['payments-list']['children'] = [];
        }

        if (!isset($newStepsLayout['afterMethods']['children'])) {
            $newStepsLayout['afterMethods']['children'] = [];
        }

        $component['billing-address-form'] = $this->getBillingAddressComponent(
            'shared',
            $elements
        );

        $newStepsLayout['afterMethods']['children'] = array_merge_recursive(
            $component,
            $newStepsLayout['afterMethods']['children']
        );

        return $newStepsLayout;
    }

    
    /**
     * Gets billing address component details
     *
     * @param string $paymentCode
     * @param array  $elements
     *
     * @return array
     */
    private function getBillingAddressComponent($paymentCode, $elements)
    {
        return [
            'component' => 'Magento_Checkout/js/view/billing-address',
            'displayArea' => 'billing-address-form-' . $paymentCode,
            'provider' => 'checkoutProvider',
            'deps' => 'checkoutProvider',
            'dataScopePrefix' => 'billingAddress' . $paymentCode,
            'sortOrder' => 1,
            'children' => [
                'form-fields' => [
                    'component' => 'uiComponent',
                    'displayArea' => 'additional-fieldsets',
                    'children' => $this->merger->merge(
                        $elements,
                        'checkoutProvider',
                        'billingAddress' . $paymentCode,
                        [
                            'country_id' => [
                                'sortOrder' => 115,
                            ],
                            'region' => [
                                'visible' => false,
                            ],
                            'region_id' => [
                                'component' => 'Magento_Ui/js/form/element/region',
                                'config' => [
                                    'template' => 'ui/form/field',
                                    'elementTmpl' => 'ui/form/element/select',
                                    'customEntry' => 'billingAddress' . $paymentCode . '.region',
                                ],
                                'validation' => [
                                    'required-entry' => true,
                                ],
                                'filterBy' => [
                                    'target' => '${ $.provider }:${ $.parentScope }.country_id',
                                    'field' => 'country_id',
                                ],
                            ],
                            'postcode' => [
                                'component' => 'Magento_Ui/js/form/element/post-code',
                                'validation' => [
                                    'required-entry' => true,
                                ],
                            ],
                            'company' => [
                                'validation' => [
                                    'min_text_length' => 0,
                                ],
                            ],
                            'fax' => [
                                'validation' => [
                                    'min_text_length' => 0,
                                ],
                            ],
                            'telephone' => [
                                'config' => [
                                    'tooltip' => [
                                        'description' => __('For delivery questions.'),
                                    ],
                                ],
                            ],
                        ]
                    ),
                ],
            ],
        ];
    }
}

Lets call our custom Webkul/CheckoutCustomization/Block/Checkout/LayoutProcessor Block.
5. create file# Webkul/CheckoutCustomization/etc/frontend/di.xml.

<?xml version="1.0"?>
<!--
/**
 * Webkul Software
 *
 * @category  Webkul
 * @package   Webkul_MarketplaceCustomization
 * @author    Webkul
 * @copyright Webkul Software Private Limited (https://webkul.com)
 * @license   https://store.webkul.com/license.html
 */
 -->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Block\Onepage">
        <arguments>
            <argument name="layoutProcessors" xsi:type="array">
                <item name="customization_billing" xsi:type="object">Webkul\CheckoutCustomization\Block\Checkout\LayoutProcessor</item>
            </argument>
        </arguments>
    </type>
</config>

6. Now overwrite https://github.com/magento/magento2/blob/2.1.9/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js file to stop Payment Page Step before our Billing Address Step

Create File Webkul/CheckoutCustomization/view/frontend/requirejs-config.js

var config = {
    map: {
        '*': {
            "Magento_Checkout/js/view/payment": "Webkul_CheckoutCustomization/js/view/payment"
        }
    }
};

Now, create this file in our CheckoutCustomization Module.
#File: Webkul/CheckoutCustomization/view/frontend/web/js/view/payment.js

/*jshint browser:true jquery:true*/
/*global alert*/
define(
    [
        'jquery',
        "underscore",
        'uiComponent',
        'ko',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/step-navigator',
        'Magento_Checkout/js/model/payment-service',
        'Magento_Checkout/js/model/payment/method-converter',
        'Magento_Checkout/js/action/get-payment-information',
        'Magento_Checkout/js/model/checkout-data-resolver',
        'mage/translate'
    ],
    function (
        $,
        _,
        Component,
        ko,
        quote,
        stepNavigator,
        paymentService,
        methodConverter,
        getPaymentInformation,
        checkoutDataResolver,
        $t
    ) {
        'use strict';

        /** Set payment methods to collection */
        paymentService.setPaymentMethods(methodConverter(window.checkoutConfig.paymentMethods));

        return Component.extend({
            defaults: {
                template: 'Magento_Checkout/payment',
                activeMethod: ''
            },
            isVisible: ko.observable(false),
            quoteIsVirtual: quote.isVirtual(),
            isPaymentMethodsAvailable: ko.computed(function () {
                return paymentService.getAvailablePaymentMethods().length > 0;
            }),

            initialize: function () {
                this._super();
                checkoutDataResolver.resolvePaymentMethod();
                stepNavigator.registerStep(
                    'payment',
                    null,
                    $t('Review & Payments'),
                    this.isVisible,
                    _.bind(this.navigate, this),
                    20
                );
                return this;
            },

            navigate: function () {
                var self = this;
                getPaymentInformation().done(function () {
                    self.isVisible(true);
                });
            },

            getFormKey: function () {
                return window.checkoutConfig.formKey;
            }
        });
    }
);

7. Finally copy https://github.com/magento/magento2/tree/2.1.9/app/code/Magento/Checkout/view/frontend/web/js/view/payment directory and place in our Billing Step Module
Webkul/CheckoutCustomization/view/frontend/web/js/view

Note: Here, we have copied the js files of payment directory from Magento 2.1.9. So, you can manage this as per your magento version.

Now Clear the cache.
It will look like this:
Checkout

Thanks

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


5 comments

  • Hemansu Umariya
    • Mahesh Singh (Moderator)
  • Dnyaneshwar Chavan
  • kavya
    • Mahesh Singh (Moderator)
  • Back to Top

    Message Sent!

    If you have more details or questions, you can reply to the received confirmation email.

    Back to Home