Magento2 | PWA | GraphQL

Move billing address just below shipping address and assign billing address from shipping step in Magento2.


In this post we will check how to move billing address right under the shipping address and assign billing address from shipping step in Magento2.

You can find complete module on Github at Magelearn_ImprovedCheckout








Let's start it by creating custom module.

Create folder inside app/code/Magelearn/ImprovedCheckout

Add registration.php file in it:

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magelearn_ImprovedCheckout',
    __DIR__
);

Add composer.json file in it:

{
    "name": "magelearn/improved-checkout",
    "description": "Move billing address just below shipping address and assign billing address from shipping step in Magento2.",
    "type": "magento2-module",
    "license": "proprietary",
    "authors": [
        {
            "name": "vijay rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [ "registration.php" ],
        "psr-4": {
            "Magelearn\\ImprovedCheckout\\": ""
        }
    }
}

Add etc/module.xml file in it:

<?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="Magelearn_ImprovedCheckout" setup_version="1.0.0">
		<sequence>
			<module name="Magento_Checkout" />
		</sequence>
	</module>
</config>
Now first we will modify Magento\Checkout\Block\Checkout\LayoutProcessor with Plugin and add some code to add billing address below the shipping address and remove Billing addresss which is display in Payment step just below Payment method.

For that first create etc/di.xml file.
<?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="Magento\Checkout\Block\Checkout\LayoutProcessor">
		<plugin name="magelearn_improved_checkout_layout_processor"
			type="Magelearn\ImprovedCheckout\Plugin\Block\LayoutProcessor"
			sortOrder="1" />
	</type>
</config>

Now add Plugin/Block/LayoutProcessor.php file.

<?php
namespace Magelearn\ImprovedCheckout\Plugin\Block;

use Magento\Customer\Model\AttributeMetadataDataProvider;
use Magento\Ui\Component\Form\AttributeMapper;
use Magento\Checkout\Block\Checkout\AttributeMerger;
use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Customer\Model\Options;

class LayoutProcessor
{

    /**
     *
     * @var AttributeMetadataDataProvider
     */
    public $attributeMetadataDataProvider;

    /**
     *
     * @var AttributeMapper
     */
    public $attributeMapper;

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

    /**
     *
     * @var CheckoutSession
     */
    public $checkoutSession;

    /**
     *
     * @var null
     */
    public $quote = null;
    
    /**
     * @var Options
     */
    public $options;
    
    /**
     * LayoutProcessor constructor.
     *
     * @param AttributeMetadataDataProvider $attributeMetadataDataProvider
     * @param AttributeMapper $attributeMapper
     * @param AttributeMerger $merger
     * @param CheckoutSession $checkoutSession
     * @param Options $options
     */
    public function __construct(
        AttributeMetadataDataProvider $attributeMetadataDataProvider,
        AttributeMapper $attributeMapper,
        AttributeMerger $merger,
        CheckoutSession $checkoutSession,
        Options $options = null
    )
    {
        $this->attributeMetadataDataProvider = $attributeMetadataDataProvider;
        $this->attributeMapper = $attributeMapper;
        $this->merger = $merger;
        $this->checkoutSession = $checkoutSession;
        $this->options = $options ?: \Magento\Framework\App\ObjectManager::getInstance()
        ->get(\Magento\Customer\Model\Options::class);
    }
    /**
     * Get Quote
     *
     * @return \Magento\Quote\Model\Quote|null
     */
    public function getQuote()
    {
        if (null === $this->quote) {
            $this->quote = $this->checkoutSession->getQuote();
        }
        
        return $this->quote;
    }
    
    /**
     *
     * @param \Magento\Checkout\Block\Checkout\LayoutProcessor $subject
     * @param array $jsLayout
     * @return array
     */
    public function aroundProcess(
        \Magento\Checkout\Block\Checkout\LayoutProcessor $subject,
        \Closure $proceed,
        array $jsLayout
    )
    {
        $jsLayoutResult = $proceed($jsLayout);

        if ($this->getQuote()->isVirtual()) {
            return $jsLayoutResult;
        }
        
        $attributesToConvert = [
            'prefix' => [$this->options, 'getNamePrefixOptions'],
            'suffix' => [$this->options, 'getNameSuffixOptions'],
        ];

        if (isset($jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']
            ['shippingAddress']['children']['shipping-address-fieldset'])) {

            $jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']
            ['shippingAddress']['children']['shipping-address-fieldset']['children']
            ['street']['children'][0]['placeholder'] = __('Street Address');
            
            $jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']
            ['shippingAddress']['children']['shipping-address-fieldset']['children']
            ['street']['children'][1]['placeholder'] = __('Street line 2');

            $jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']
            ['shippingAddress']['children']['billing-address']['children']['form-fields']['children']
            ['street']['children'][0]['placeholder'] = __('Street Address');
            
            $jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']
            ['shippingAddress']['children']['billing-address']['children']['form-fields']['children']
            ['street']['children'][1]['placeholder'] = __('Street line 2');
        }

        $elements = $this->getAddressAttributes();
        $elements = $this->convertElementsToSelect($elements, $attributesToConvert);

        $jsLayoutResult['components']['checkout']['children']['steps']['children']['shipping-step']['children']['shippingAddress']
        ['children']['billingAddress']['children']['address-fieldset'] = $this->getCustomBillingAddressComponent($elements);

        if (isset($jsLayoutResult['components']['checkout']['children']['steps']['children']['billing-step']['children']
            ['payment']['children']['afterMethods']['children']['billing-address-form'])) {
            unset($jsLayoutResult['components']['checkout']['children']['steps']['children']['billing-step']['children']
                ['payment']['children']['afterMethods']['children']['billing-address-form']);
        }

        if ($billingAddressForms = $jsLayoutResult['components']['checkout']['children']['steps']['children']
            ['billing-step']['children']['payment']['children']['payments-list']['children']) {
            foreach ($billingAddressForms as $billingAddressFormsKey => $billingAddressForm) {
                if ($billingAddressFormsKey != 'before-place-order') {
                    unset($jsLayoutResult['components']['checkout']['children']['steps']['children']['billing-step']['children']
                        ['payment']['children']['payments-list']['children'][$billingAddressFormsKey]);
                }
            }
        }

        return $jsLayoutResult;
    }

    

    /**
     * Get all visible address attribute
     *
     * @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;
    }

    /**
     * Prepare billing address field for shipping step for physical product
     *
     * @param
     *            $elements
     * @return array
     */
    public function getCustomBillingAddressComponent($elements)
    {
        $providerName = 'checkoutProvider';

        $components = [
            'component' => 'uiComponent',
            'displayArea' => 'additional-fieldsets',
            'children' => $this->merger->merge($elements, $providerName, 'billingAddress', [
                '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.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.')
                        ]
                    ]
                ]
            ])
        ];

        return $components;
    }
    
    private function convertElementsToSelect($elements, $attributesToConvert)
    {
        $codes = array_keys($attributesToConvert);
        foreach (array_keys($elements) as $code) {
            if (!in_array($code, $codes)) {
                continue;
            }
            // phpcs:ignore Magento2.Functions.DiscouragedFunction
            $options = call_user_func($attributesToConvert[$code]);
            if (!is_array($options)) {
                continue;
            }
            $elements[$code]['dataType'] = 'checkbox-set';
            $elements[$code]['formElement'] = 'checkbox-set';
            $elements[$code]['value'] = '0';
            
            foreach ($options as $key => $value) {
                $elements[$code]['options'][] = [
                    'value' => $key,
                    'label' => $value
                ];
            }
        }
        return $elements;
    }
}

Now we will modify checkout_index_index.xml file and modify it by adding some child node just below shipping address to display billing address below it.

Add file at 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">
	<update handle="styles"/>
    <head>
        <css src="Magelearn_ImprovedCheckout::css/improvedcheckout.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">
										<item name="shipping-step" xsi:type="array">
											<item name="children" xsi:type="array">
												<item name="shippingAddress" xsi:type="array">
													<item name="children" xsi:type="array">
														<item name="billingAddress" xsi:type="array">
															<item name="config" xsi:type="array">
																<item name="deps" xsi:type="array">
																	<item name="0" xsi:type="string">checkout.steps.shipping-step.step-config</item>
																	<item name="1" xsi:type="string">checkoutProvider</item>
																</item>
																<item name="popUpForm" xsi:type="array">
																	<item name="element" xsi:type="string">#opc-new-billing-address</item>
																	<item name="options" xsi:type="array">
																		<item name="type" xsi:type="string">popup</item>
																		<item name="responsive" xsi:type="boolean">true</item>
																		<item name="innerScroll" xsi:type="boolean">true</item>
																		<item name="title" xsi:type="string"
																			translate="true">Billing Address</item>
																		<item name="trigger" xsi:type="string">opc-new-billing-address</item>
																		<item name="buttons" xsi:type="array">
																			<item name="save" xsi:type="array">
																				<item name="text" xsi:type="string" translate="true">Save Address</item>
																				<item name="class" xsi:type="string">action primary action-save-address</item>
																			</item>
																			<item name="cancel" xsi:type="array">
																				<item name="text" xsi:type="string"
																					translate="true">Cancel</item>
																				<item name="class" xsi:type="string">action secondary action-hide-popup</item>
																			</item>
																		</item>
																	</item>
																</item>
															</item>
															<item name="component" xsi:type="string">Magelearn_ImprovedCheckout/js/view/billing</item>
															<item name="displayArea" xsi:type="string">billing-address</item>
															<item name="provider" xsi:type="string">checkoutProvider</item>
															<item name="sortOrder" xsi:type="string">1</item>
															<item name="children" xsi:type="array">
																<item name="before-form" xsi:type="array">
																	<item name="component" xsi:type="string">uiComponent</item>
																	<item name="displayArea" xsi:type="string">billing-before-form</item>
																	<item name="children" xsi:type="array">
																		<!-- before form fields -->
																	</item>
																</item>
																<item name="before-fields" xsi:type="array">
																	<item name="component" xsi:type="string">uiComponent</item>
																	<item name="displayArea" xsi:type="string">billing-before-fields</item>
																	<item name="children" xsi:type="array">
																		<!-- before fields -->
																	</item>
																</item>
																<item name="address-list" xsi:type="array">
																	<item name="component" xsi:type="string">Magelearn_ImprovedCheckout/js/view/billing-address/list</item>
																	<item name="config" xsi:type="array">
																		<item name="template" xsi:type="string">Magelearn_ImprovedCheckout/billing-address/custom-list</item>
																	</item>
																	<item name="displayArea" xsi:type="string">billing-address-list</item>
																</item>
															</item>
														</item>
													</item>
												</item>
											</item>
										</item>
									</item>
								</item>
							</item>
						</item>
					</item>
				</argument>
			</arguments>
		</referenceBlock>
	</body>
</page>

Now as per highlighted code above first we will create our JS component file at view/frontend/web/js/view/billing.js

define([
	    'jquery',
	    'underscore',
	    'Magento_Ui/js/form/form',
	    'ko',	    
	    'Magento_Customer/js/model/customer',
	    'Magento_Customer/js/model/address-list',
	    'Magento_Checkout/js/model/address-converter',
	    'Magento_Checkout/js/model/quote',
	    'Magelearn_ImprovedCheckout/js/model/billing-address/form-popup-state',
	    'Magelearn_ImprovedCheckout/js/action/edit-billing-address',
	    'Magento_Checkout/js/action/create-billing-address',
	    'Magento_Checkout/js/action/select-billing-address',
	    'Magento_Checkout/js/checkout-data',
	    'Magento_Checkout/js/model/checkout-data-resolver',
	    'Magento_Customer/js/customer-data',
	    'Magento_Checkout/js/action/set-billing-address',
	    'Magento_Ui/js/modal/modal',
	    'Magento_Ui/js/model/messageList',
	    'mage/translate',
	    'mage/url'
	], function (
	    $,
	    _,
	    Component,
	    ko,
	    customer,
	    addressList,
	    addressConverter,
	    quote,
	    formPopUpState,
	    editBillingAddress,
	    createBillingAddress,
	    selectBillingAddress,
	    checkoutData,
	    checkoutDataResolver,
	    customerData,
	    setBillingAddressAction,
	    modal,
	    globalMessageList,
	    $t,
	    url
	) {
	    'use strict';

	    var popUp = null,
    		addressOptions = addressList().filter(function (address) {
                return address.getType() === 'customer-address';
            });

	    return Component.extend({
			defaults: {
			    template: 'Magelearn_ImprovedCheckout/billing',
			    billingFormTemplate: 'Magelearn_ImprovedCheckout/billing-address/form'
			},
			visible: ko.observable(!quote.isVirtual()),
			errorValidationMessage: ko.observable(false),
			isCustomerLoggedIn: customer.isLoggedIn,
			isFormPopUpVisible: formPopUpState.isVisible,
			isFormInline: addressList().length === 0,
			isNewAddressAdded: ko.observable(false),
			saveInAddressBook: 1,
			quoteIsVirtual: quote.isVirtual(),
			customerHasAddresses: addressOptions.length > 0,
	
			/**
			 * @return {exports}
			 */
	
			/**
			 * Init component
			 */
			initialize: function () {
			    this._super();
			    quote.paymentMethod.subscribe(function () {
			        checkoutDataResolver.resolveBillingAddress();
			    }, this);
			},
	
			/**
			 * @return {exports.initObservable}
			 */
			initObservable: function () {
			    var self = this,
			        hasNewAddress;
	
			    this._super()
			        .observe({
			            selectedAddress: null,
			            isAddressFormVisible: true,
			            isAddressSameAsShipping: false,
			            isAddressFormListVisible: false,
			            saveInAddressBook: 1
			        });
	
			    quote.billingAddress.subscribe(function (newAddress) {
			        if (quote.isVirtual()) {
			            this.isAddressSameAsShipping(false);
			        } else {
			            this.isAddressSameAsShipping(
			                newAddress != null && quote.shippingAddress()
			                && newAddress.getCacheKey() === quote.shippingAddress().getCacheKey() //eslint-disable-line eqeqeq
			            );
			        }
	
			        if (newAddress != null && newAddress.saveInAddressBook !== undefined) {
			            this.saveInAddressBook(newAddress.saveInAddressBook);
			        } else {
			            this.saveInAddressBook(1);
			        }
			    }, this);
	
			    checkoutDataResolver.resolveBillingAddress();
	
			    hasNewAddress = addressList.some(function (address) {
			        return address.getType() === 'new-customer-address' || address.getType() === 'new-billing-address'; //eslint-disable-line eqeqeq
			    });
	
			    this.isNewAddressAdded(hasNewAddress);
	
			    this.isFormPopUpVisible.subscribe(function (value) {
			        if (value) {
			            self.getPopUp().openModal();
			        }
			    });
	
			    return this;
			},
	
			/**
			 * Navigator change hash handler.
			 *
			 * @param {Object} step - navigation step
			 */
			navigate: function (step) {
			    step && step.isVisible(true);
			},
	
			/**
			 * @return {*}
			 */
			getPopUp: function () {
			    var self = this,
			        buttons;
	
			    if (!popUp) {
			        buttons = this.popUpForm.options.buttons;
			        this.popUpForm.options.buttons = [
			            {
			                text: buttons.save.text ? buttons.save.text : $t('Save Address'),
			                class: buttons.save.class ? buttons.save.class : 'action primary action-save-address',
			                click: self.saveNewAddress.bind(self)
			            },
			            {
			                text: buttons.cancel.text ? buttons.cancel.text : $t('Cancel'),
			                class: buttons.cancel.class ? buttons.cancel.class : 'action secondary action-hide-popup',
	
			                /** @inheritdoc */
			                click: this.onClosePopUp.bind(this)
			            }
			        ];
	
			        /** @inheritdoc */
			        this.popUpForm.options.closed = function () {
			            self.isFormPopUpVisible(false);
			        };
	
			        this.popUpForm.options.modalCloseBtnHandler = this.onClosePopUp.bind(this);
			        this.popUpForm.options.keyEventHandlers = {
			            escapeKey: this.onClosePopUp.bind(this)
			        };
	
			        /** @inheritdoc */
			        this.popUpForm.options.opened = function () {
			            // Store temporary address for revert action in case when user click cancel action
			            self.temporaryAddress = $.extend(true, {}, checkoutData.getBillingAddressFromData());
			        };
			        popUp = modal(this.popUpForm.options, $(this.popUpForm.element));
			    }
	
			    return popUp;
			},
	
			/**
			 * Revert address and close modal.
			 */
			onClosePopUp: function () {
			    checkoutData.getBillingAddressFromData($.extend(true, {}, this.temporaryAddress));
			    this.getPopUp().closeModal();
			},
	
			/**
			 * Show address form popup
			 */
			showFormPopUp: function () {
			    this.isFormPopUpVisible(true);
			},
	
			/**
			 * Save new billing address
			 */
			saveNewAddress: function () {
			    var addressData,
			        newBillingAddress;
	
			    this.source.set('params.invalid', false);
			    this.triggerBillingDataValidateEvent();
	
			    if (!this.source.get('params.invalid')) {
			        addressData = this.source.get('billingAddress');
			        // if user clicked the checkbox, its value is true or false. Need to convert.
			        addressData['save_in_address_book'] = this.saveInAddressBook ? 1 : 0;
	
			        // New address must be selected as a billing address
			        newBillingAddress = editBillingAddress(addressData);
			        selectBillingAddress(newBillingAddress);
			        checkoutData.setSelectedBillingAddress(newBillingAddress.getKey());
			        checkoutData.setNewCustomerBillingAddress($.extend(true, {}, addressData));
			        this.getPopUp().closeModal();
			        this.isNewAddressAdded(true);
			    }
			},
	
			/**
			 * Trigger Billing data Validate Event.
			 */
			triggerBillingDataValidateEvent: function () {
			    this.source.trigger('billingAddress.data.validate');
	
			    if (this.source.get('billingAddress.custom_attributes')) {
			        this.source.trigger('billingAddress.custom_attributes.data.validate');
			    }
			},
	    });
});

Now as per highlighted code above we will add our necessary JS files.

Add file at view/frontend/web/js/model/billing-address/form-popup-state.js

define([
	'ko'
], function (ko) {
    'use strict';

    return {
    	isVisible: ko.observable(false)
    };
});

Add file at view/frontend/web/js/action/edit-billing-address.js 

This file is responsible to Append new address to the address list after adding a New Billing Address.

define([
	    'Magento_Customer/js/model/address-list',
	    'Magento_Checkout/js/model/address-converter'
	], function (addressList,addressConverter) {
	    'use strict';

	    return function (addressData) {
	    	var address = addressConverter.formAddressDataToQuoteAddress(addressData),
    			isAddressUpdated = addressList().some(function (currentAddress, index, addresses) {
                    if (currentAddress.getKey() == address.getKey()) { //eslint-disable-line eqeqeq
                        addresses[index] = address;

                        return true;
                    }

                    return false;
                });

	    	if (!isAddressUpdated) {
	            addressList.push(address);
	        } else {
	            addressList.valueHasMutated();
	        }

	    	return address;
	};
});

Now as per highlighted code in billing.js file we will add our template file as well as billing form template file.

Add file at view/frontend/web/template/billing.html

<div class="checkout-billing-address">
    <!-- ko foreach: getRegion('billing-address-list') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!--/ko-->

    <!-- Inline address form -->
    <render if="isFormInline" args="billingFormTemplate"></render>
</div>

<!-- Address form pop up -->
<if args="!isFormInline">
	<div class="new-address-popup">
	    <button type="button"
		    class="action action-show-popup"
		    click="showFormPopUp"
		    visible="!isNewAddressAdded()">
			<span translate="'New Address'"></span>
	    </button>
    </div>
    <div id="opc-new-billing-address"
	 visible="isFormPopUpVisible()"
	 render="billingFormTemplate"></div>
</if>

Add file at view/frontend/web/template/billing-address/form.html

<div class="billing-address-form" data-bind="fadeVisible: isAddressFormVisible">
	<!-- ko foreach: getRegion('before-fields') -->
	<!-- ko template: getTemplate() -->
	<!-- /ko -->
	<!--/ko-->
	<fieldset class="fieldset address" data-form="billing-new-address">
		<!-- ko foreach: getRegion('additional-fieldsets') -->
		<!-- ko template: getTemplate() -->
		<!-- /ko -->
		<!--/ko-->
		<!-- ko if: (isCustomerLoggedIn && customerHasAddresses) -->
		<div class="choice field">
			<input type="checkbox" class="checkbox"
				data-bind="checked: saveInAddressBook, attr: {id: 'billing-save-in-address-book'}" />
			<label class="label" data-bind="attr: {for: 'billing-save-in-address-book'}">
				<span data-bind="i18n: 'Save in address book'"></span>
			</label>
		</div>
		<!-- /ko -->
	</fieldset>
</div>

Now we will add our billing address list component JS file and Billing address list items template file.

First add our billing address list JS component file at view/frontend/web/js/view/billing-address/list.js

define([
    'underscore',
    'ko',
    'mageUtils',
    'uiComponent',
    'uiLayout',
    'Magento_Customer/js/model/address-list'
], function (_, ko, utils, Component, layout, addressList) {
    'use strict';

    var defaultRendererTemplate = {
		parent: '${ $.$data.parentName }',
		name: '${ $.$data.name }',
		component: 'Magelearn_ImprovedCheckout/js/view/billing-address/address-renderer/default',
		provider: 'checkoutProvider'
    };

    return Component.extend({
		defaults: {
		    template: 'Magelearn_ImprovedCheckout/billing-address/list',
		    visible: addressList().length > 0,
		    rendererTemplates: []
		},
	
		/** @inheritdoc */
		initialize: function () {
		    this._super()
		        .initChildren();
	
		    addressList.subscribe(function (changes) {
		            var self = this;
	
		            changes.forEach(function (change) {
		                if (change.status === 'added') {
		                    self.createRendererComponent(change.value, change.index);
		                }
		            });
		        },
		        this,
		        'arrayChange'
		    );
	
		    return this;
		},
	
		/** @inheritdoc */
		initConfig: function () {
		    this._super();
		    this.rendererComponents = [];
	
		    return this;
		},
	
		/** @inheritdoc */
		initChildren: function () {
		    _.each(addressList(), this.createRendererComponent, this);
	
		    return this;
		},
	
		/**
		 * Create new component that will render given address in the address list
		 *
		 * @param {Object} address
		 * @param {*} index
		 */
		createRendererComponent: function (address, index) {
		    var rendererTemplate, templateData, rendererComponent;
	
		    if (index in this.rendererComponents) {
		        this.rendererComponents[index].address(address);
		    } else {
		        // rendererTemplates are provided via layout
		        rendererTemplate = address.getType() != undefined && this.rendererTemplates[address.getType()] != undefined ? //eslint-disable-line
		            utils.extend({}, defaultRendererTemplate, this.rendererTemplates[address.getType()]) :
		            defaultRendererTemplate;
		        templateData = {
		            parentName: this.name,
		            name: index
		        };
		        rendererComponent = utils.template(rendererTemplate, templateData);
		        utils.extend(rendererComponent, {
		            address: ko.observable(address)
		        });
		        layout([rendererComponent]);
		        this.rendererComponents[index] = rendererComponent;
		    }
		}
    });
});

And billing address Item's template file at view/frontend/web/template/billing-address/custom-list.html

<!-- ko if: (visible)-->
	<div class="field addresses">
	    <div class="control">
		<div class="billing-address-items">
		    <!-- ko foreach: { data: elems, as: 'element' } -->
		    <!-- ko template: element.getTemplate() --><!-- /ko -->
		    <!-- /ko -->
		</div>
	    </div>
	</div>
<!-- /ko -->

Now as per highlighted in list.js file we will add JS component and template file.

Add file at view/frontend/web/js/view/billing-address/address-renderer/default.js

define([
    'jquery',
    'ko',
    'uiComponent',
    'Magento_Checkout/js/action/select-billing-address',
    'Magento_Checkout/js/model/quote',
    'Magelearn_ImprovedCheckout/js/model/billing-address/form-popup-state',
    'Magento_Checkout/js/checkout-data',
    'Magento_Customer/js/customer-data'
], function ($, ko, Component, selectBillingAddressAction, quote, formPopUpState, checkoutData, customerData) {
	    'use strict';
	
	    var countryData = customerData.get('directory-data');
	
	    return Component.extend({
		defaults: {
		    template: 'Magelearn_ImprovedCheckout/billing-address/address-renderer/default'
		},
	
		/** @inheritdoc */
		initObservable: function () {
		    this._super();
		    this.isSelected = ko.computed(function () {
		        var isSelected = false,
		            billingAddress = quote.billingAddress();
	
		        if (billingAddress) {
		            isSelected = billingAddress.getKey() == this.address().getKey(); //eslint-disable-line eqeqeq
		        }
	
		        return isSelected;
		    }, this);
	
		    return this;
		},
	
		/**
		 * @param {String} countryId
		 * @return {String}
		 */
		getCountryName: function (countryId) {
		    return countryData()[countryId] != undefined ? countryData()[countryId].name : ''; //eslint-disable-line
		},
		
		getCustomAttributeLabel: function (attribute) {
	        var label;
	
	        if (typeof attribute === 'string') {
	            return attribute;
	        }
	
	        if (attribute.label) {
	            return attribute.label;
	        }
	
	        if (_.isArray(attribute.value)) {
	            label = _.map(attribute.value, function (value) {
	                return this.getCustomAttributeOptionLabel(attribute['attribute_code'], value) || value;
	            }, this).join(', ');
	        } else if (typeof attribute.value === 'object') {
	            label = _.map(Object.values(attribute.value)).join(', ');
	        } else {
	            label = this.getCustomAttributeOptionLabel(attribute['attribute_code'], attribute.value);
	        }
	
	        return label || attribute.value;
	    },
		
		getCustomAttributeOptionLabel: function (attributeCode, value) {
	        var option,
	            label,
	            options = this.source.get('customAttributes') || {};
	
	        if (options[attributeCode]) {
	            option = _.findWhere(options[attributeCode], {
	                value: value
	            });
	
	            if (option) {
	                label = option.label;
	            }
	        } else if (value.file !== null) {
	            label = value.file;
	        }
	
	        return label;
	    },
		
		/** Set selected customer shipping address  */
		selectAddress: function () {
		    selectBillingAddressAction(this.address());
		    checkoutData.setSelectedBillingAddress(this.address().getKey());
		},
	
		/**
		 * Edit address.
		 */
		editAddress: function () {
		    formPopUpState.isVisible(true);
		    this.showPopup();
	
		},
	
		/**
		 * Show popup.
		 */
		showPopup: function () {
		    $('[data-open-modal="opc-new-billing-address"]').trigger('click');
		}
    });
});

Now as per highlighted code in above file, we will add our default address render file.

Add file at view/frontend/web/template/billing-address/address-renderer/default.html

<div class="billing-address-item" css="'selected-item' : isSelected() , 'not-selected-item':!isSelected()">
	<text args="address().prefix"></text> <text args="address().firstname"></text> <text args="address().middlename"></text>
	<text args="address().lastname"></text> <text args="address().suffix"></text><br/>
	<text args="_.values(address().street).join(', ')"></text><br/>
	<text args="address().city "></text>, <span text="address().region"></span> <text args="address().postcode"></text><br/>
	<text args="getCountryName(address().countryId)"></text><br/>
	<a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/>
	
	<each args="data: address().customAttributes, as: 'element'">
        <text args="$parent.getCustomAttributeLabel(element)"></text>
        <br/>
    </each>

    <button visible="address().isEditable()" type="button"
            class="action edit-address-link"
            click="editAddress">
        <span translate="'Edit'"></span>
    </button>
    <!-- ko if: (!isSelected()) -->
    <button type="button" click="selectAddress" class="action action-select-billing-item">
        <span translate="'Bill Here'"></span>
    </button>
    <!-- /ko -->
</div>

And will also add billing address list template file at view/frontend/web/template/billing-address/list.html

<!-- ko foreach: getRegion('before-fields') -->
   <!-- ko template: getTemplate() --><!-- /ko -->
   <!--/ko-->
   <form data-bind="attr: {'data-hasrequired': $t('* Required Fields')}">
       <fieldset id="billing-new-address-form" class="fieldset address">
           <!-- ko foreach: getRegion('additional-fieldsets') -->
           <!-- ko template: getTemplate() --><!-- /ko -->
           <!--/ko-->
           <!-- ko if: (isCustomerLoggedIn) -->
           <div class="field save-address">
               <input type="checkbox" class="checkbox" 
               	data-bind="checked: saveInAddressBook attr: {id: 'billing-save-in-address-book'}" />
               <label class="label" data-bind="attr: {for: 'billing-save-in-address-book'}">
                   <span data-bind="i18n: 'Save in address book'"></span>
               </label>
           </div>
           <!-- /ko -->
       </fieldset>
   </form>

At last we will add our shipping mixin file.
For that first add file at view/frontend/requirejs-config.js

var config = {
    config: {
		mixins: {
		    'Magento_Checkout/js/view/shipping': {
		        'Magelearn_ImprovedCheckout/js/view/shipping-mixin': true
		    },
		}
    }
};

Now add file at view/frontend/web/js/view/shipping-mixin.js

define([
	    'jquery',
	    'underscore',
	    'Magento_Ui/js/form/form',
	    'ko',
	    'Magento_Customer/js/model/customer',
	    'Magento_Customer/js/model/address-list',
	    'Magento_Checkout/js/model/address-converter',
	    'Magento_Checkout/js/model/quote',
	    'Magento_Checkout/js/action/create-shipping-address',
	    'Magento_Checkout/js/action/select-shipping-address',
	    'Magento_Checkout/js/action/create-billing-address',
	    'Magento_Checkout/js/action/select-billing-address',
	    'Magento_Checkout/js/model/shipping-rates-validator',
	    'Magento_Checkout/js/model/shipping-address/form-popup-state',
	    'Magento_Checkout/js/model/shipping-service',
	    'Magento_Checkout/js/action/select-shipping-method',
	    'Magento_Checkout/js/model/shipping-rate-registry',
	    'Magento_Checkout/js/action/set-shipping-information',
	    'Magento_Checkout/js/model/step-navigator',
	    'Magento_Ui/js/modal/modal',
	    'Magento_Checkout/js/model/checkout-data-resolver',
	    'Magento_Checkout/js/checkout-data',
	    'uiRegistry',
	    'mage/translate',
	    'Magento_Checkout/js/model/shipping-rate-service'
	], function (
	    $,
	    _,
	    Component,
	    ko,
	    customer,
	    addressList,
	    addressConverter,
	    quote,
	    createShippingAddress,
	    selectShippingAddress,
	    createBillingAddress,
	    selectBillingAddress,
	    shippingRatesValidator,
	    formPopUpState,
	    shippingService,
	    selectShippingMethodAction,
	    rateRegistry,
	    setShippingInformationAction,
	    stepNavigator,
	    modal,
	    checkoutDataResolver,
	    checkoutData,
	    registry,
	    $t
	) {
	    'use strict';

	    var popUp = null;

	    return function (Component) {
			return Component.extend({
			    defaults: {
			        template: 'Magelearn_ImprovedCheckout/shipping',
			    },
			    isAddressSameAsShipping: ko.observable(true),
			    isShowBillingForm: ko.observable(false),
	
			    initChildren: function () {
			        this.messageContainer = new Messages();
			        this.createMessagesComponent();
			        return this;
			    },
	
			    createMessagesComponent: function () {
			        var messagesComponent = {
			            parent: this.name,
			            name: this.name + '.messages',
			            displayArea: 'messages',
			            component: 'Magento_Ui/js/view/messages',
			            config: {
			                messageContainer: this.messageContainer
			            }
			        };
	
			        layout([messagesComponent]);
	
			        return this;
			    },
	
			    setShippingInformation: function () {
			        if (this.validateShippingInformation()
			            && this.validateBillingInformation()) {
			            setShippingInformationAction().done(function () {
			                stepNavigator.next();
			            });
			        }
			    },
	
			    /**
			     * @return {Boolean}
			     */
			    validateShippingInformation: function () {
			        var shippingAddress,
			            addressData,
			            loginFormSelector = 'form[data-role=email-with-possible-login]',
			            emailValidationResult = customer.isLoggedIn(),
			            field,
			            option = _.isObject(this.countryOptions) && this.countryOptions[quote.shippingAddress().countryId],
		                messageContainer = registry.get('checkout.errors').messageContainer;
	
			        if (!quote.shippingMethod()) {
			            this.errorValidationMessage($t('Please specify a shipping method.'));
	
			            return false;
			        }
	
			        if (!customer.isLoggedIn()) {
			            $(loginFormSelector).validation();
			            emailValidationResult = Boolean($(loginFormSelector + ' input[name=username]').valid());
			        }
	
			        if (this.isFormInline) {
			            this.source.set('params.invalid', false);
			            this.triggerShippingDataValidateEvent();
	
			            if (emailValidationResult &&
			                this.source.get('params.invalid') ||
			                !quote.shippingMethod()['method_code'] ||
			                !quote.shippingMethod()['carrier_code']
			            ) {
			                this.focusInvalid();
	
			                return false;
			            }
	
			            shippingAddress = quote.shippingAddress();
			            addressData = addressConverter.formAddressDataToQuoteAddress(
			                this.source.get('shippingAddress')
			            );
	
			            //Copy form data to quote shipping address object
			            for (field in addressData) {
			                if (addressData.hasOwnProperty(field) &&  //eslint-disable-line max-depth
			                    shippingAddress.hasOwnProperty(field) &&
			                    typeof addressData[field] != 'function' &&
			                    _.isEqual(shippingAddress[field], addressData[field])
			                ) {
			                    shippingAddress[field] = addressData[field];
			                } else if (typeof addressData[field] != 'function' &&
			                    !_.isEqual(shippingAddress[field], addressData[field])) {
			                    shippingAddress = addressData;
			                    break;
			                }
			            }
	
			            if (customer.isLoggedIn()) {
			                shippingAddress['save_in_address_book'] = 1;
			            }
			            selectShippingAddress(shippingAddress);
			        } else if (customer.isLoggedIn() &&
		                option &&
		                option['is_region_required'] &&
		                !quote.shippingAddress().region
		            ) {
		                messageContainer.addErrorMessage({
		                    message: $t('Please specify a regionId in shipping address.')
		                });

		                return false;
		            }
	
			        if (!emailValidationResult) {
			            $(loginFormSelector + ' input[name=username]').focus();
	
			            return false;
			        }
	
			        return true;
			    },
	
			    validateBillingInformation: function () {
			        var addressData, newBillingAddress;
	
			        if ($('[name="billing-address-same-as-shipping"]').is(":checked")) {
			            if (this.isFormInline) {
			                var shippingAddress = quote.shippingAddress();
			                addressData = addressConverter.formAddressDataToQuoteAddress(
			                    this.source.get('shippingAddress')
			                );
			                //Copy form data to quote shipping address object
			                for (var field in addressData) {
			                    if (addressData.hasOwnProperty(field) &&
			                        shippingAddress.hasOwnProperty(field) &&
			                        typeof addressData[field] !== 'function' &&
			                        _.isEqual(shippingAddress[field], addressData[field])
			                    ) {
			                        shippingAddress[field] = addressData[field];
			                    } else if (typeof addressData[field] !== 'function' &&
			                        !_.isEqual(shippingAddress[field], addressData[field])) {
			                        shippingAddress = addressData;
			                        break;
			                    }
			                }
	
			                if (customer.isLoggedIn()) {
			                    shippingAddress.save_in_address_book = 1;
			                }
			                newBillingAddress = createBillingAddress(shippingAddress);
			                selectBillingAddress(newBillingAddress);
			            } else {
			                var billingAddress = quote.shippingAddress();
			                selectBillingAddress(billingAddress);
			            }
	
			            return true;
			        }
	
			        var selectedAddress = quote.billingAddress();
			        if (selectedAddress) {
			            if (selectedAddress.customerAddressId) {
			                return addressList.some(function (address) {
			                    if (selectedAddress.customerAddressId === address.customerAddressId) {
			                        selectBillingAddress(address);
			                        return true;
			                    }
			                    return false;
			                });
			            } else if (selectedAddress.getType() === 'new-customer-address' || selectedAddress.getType() === 'new-billing-address') {
			                return true;
			            }
			        }
	
			        this.source.set('params.invalid', false);
			        this.source.trigger('billingAddress.data.validate');
	
			        if (this.source.get('billingAddress.custom_attributes')) {
			            this.source.trigger('billingAddress.custom_attributes.data.validate');
			        }
	
			        if (this.source.get('params.invalid')) {
			            return false;
			        }
	
			        addressData = this.source.get('billingAddress');
	
			        if ($('#billing-save-in-address-book').is(":checked")) {
			            addressData.save_in_address_book = 1;
			        }
			        newBillingAddress = createBillingAddress(addressData);
	
			        selectBillingAddress(newBillingAddress);
	
			        return true;
			    },
	
			    /**
			     * @return {Boolean}
			     */
			    useShippingAddress: function () {
			        if (this.isAddressSameAsShipping()) {
			            this.isShowBillingForm(false);
			        } else {
			            this.isShowBillingForm(true);
			        }
			        return true;
			    },
	
			});
	    }
});

Now as per highlighted code above, we will add our template file at view/frontend/web/template/shipping.html

<li id="shipping" class="checkout-shipping-address" data-bind="fadeVisible: visible()">
    <div class="step-title" translate="'Shipping Address'" data-role="title"></div>
    <div id="checkout-step-shipping"
	 class="step-content"
	 data-role="content">

	<each if="!quoteIsVirtual" args="getRegion('customer-email')" render="" ></each>
    <each args="getRegion('address-list')" render="" ></each>
    <each args="getRegion('address-list-additional-addresses')" render="" ></each>

	<!-- Address form pop up -->
	<if args="!isFormInline">
        <div class="new-address-popup">
            <button type="button"
                    class="action action-show-popup"
                    click="showFormPopUp"
                    visible="!isNewAddressAdded()">
                <span translate="'New Address'"></span>
            </button>
        </div>
        <div id="opc-new-shipping-address"
             visible="isFormPopUpVisible()"
             render="shippingFormTemplate"></div>
    </if>

	<each args="getRegion('before-form')" render="" ></each>

	<!-- Inline address form -->
	<render if="isFormInline" args="shippingFormTemplate"></render>

	<div class="step-title" translate="'Billing Address'" data-role="title"></div>
    <div id="checkout-step-billing"
         class="step-content"
         data-role="content">

        <div id="billing-address-container">
        	<div class="billing-address-same-as-shipping-block field choice">
		        <input type="checkbox" name="billing-address-same-as-shipping"
		               data-bind="checked: isAddressSameAsShipping, click: useShippingAddress, attr: {id: 'billing-address-same-as-shipping-shared'}"/>
		        <label data-bind="attr: {for: 'billing-address-same-as-shipping-shared'}"><span
		                data-bind="i18n: 'Billing address is same as Shipping address (uncheck if you want to use different address).'"></span></label>
		    </div>
		    <div class="form-billing-address" data-bind="visible: isShowBillingForm">
		        <each args="getRegion('billing-address')" render="" ></each>
		    </div>
		</div>
        <!-- Address form pop up -->
        
    </div>
    </div>
</li>

<!--Shipping method template-->
<li id="opc-shipping_method"
    class="checkout-shipping-method"
    data-bind="fadeVisible: visible(), blockLoader: isLoading"
    role="presentation">
    <div class="checkout-shipping-method">
        <div class="step-title"
             translate="'Shipping Methods'"
             data-role="title"></div>

        <each args="getRegion('before-shipping-method-form')" render="" ></each>

        <div id="checkout-step-shipping_method"
             class="step-content"
             data-role="content"
             role="tabpanel"
             aria-hidden="false">
            <form id="co-shipping-method-form"
                  class="form methods-shipping"
                  if="rates().length"
                  submit="setShippingInformation"
                  novalidate="novalidate">

                <render args="shippingMethodListTemplate"></render>

                <div id="onepage-checkout-shipping-method-additional-load">
                    <each args="getRegion('shippingAdditional')" render="" ></each>
                </div>
                <div role="alert"
                     if="errorValidationMessage().length"
                     class="message notice">
                    <span text="errorValidationMessage()"></span>
                </div>
                <div class="actions-toolbar" id="shipping-method-buttons-container">
                    <div class="primary">
                        <button data-role="opc-continue" type="submit" class="button action continue primary">
                            <span translate="'Next'"></span>
                        </button>
                    </div>
                </div>
            </form>
            <div class="no-quotes-block"
                 ifnot="rates().length > 0"
                 translate="'Sorry, no quotes are available for this order at this time'"></div>
        </div>
    </div>
</li>

At last as per highlighted in checkout_index_index.xml we will add our css file at view/frontend/web/css/improvedcheckout.css to display billing address data properly.

0 Comments On "Move billing address just below shipping address and assign billing address from shipping step in Magento2."

Back To Top