Magento 2 – Fix Shipping Method Prices Not Showing in Checkout

checkoutknockoutjsmagento2moduletemplate

I would like to add a new Checkout step to the Checkout page but would like to place it between the customer's shipping details (name, address, email) and the shipping methods radio buttons. To do this, I have removed the Shipping Methods radio buttons (li#opc-shipping_method) from shipping.htmland placed it in a custom module. Apart from removing the shipping methods, I haven't touched the shipping.html file.

#Magento_Checkout/web/template/shipping.html

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

    <!-- ko if: (!quoteIsVirtual) -->
        <!-- ko foreach: getRegion('customer-email') -->
            <!-- ko template: getTemplate() --><!-- /ko -->
        <!--/ko-->
    <!--/ko-->

    <!-- ko foreach: getRegion('address-list') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!--/ko-->

    <!-- ko foreach: getRegion('address-list-additional-addresses') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!--/ko-->

    <!-- Address form pop up -->
    <!-- ko if: (!isFormInline) -->
    <button type="button"
            data-bind="click: showFormPopUp, visible: !isNewAddressAdded()"
            class="action action-show-popup">
        <span data-bind="i18n: 'New Address'"></span></button>
    <div id="opc-new-shipping-address" data-bind="visible: isFormPopUpVisible()">
        <!-- ko template: 'Magento_Checkout/shipping-address/form' --><!-- /ko -->
    </div>
    <!-- /ko -->

    <!-- ko foreach: getRegion('before-form') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!--/ko-->

    <!-- Inline address form -->
    <!-- ko if: (isFormInline) -->
    <!-- ko template: 'Magento_Checkout/shipping-address/form' --><!-- /ko -->
    <!-- /ko -->
</div>

Before adding my new step between the shipping details and the shipping methods, I enabled the new module containing the shipping methods to make sure all was okay. The shipping methods show on the checkout as exepcted, but the prices aren't being displayed!
enter image description here

My new module is made up of the following:

#Mike/Checkout/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="Mike_Checkout" setup_version="1.0.0"/>
</config>

#Mike/Checkout/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">
<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-methods" xsi:type="array">
                                            <item name="component" xsi:type="string">Mike_Checkout/js/view/shipping-methods</item>
                                            <item name="sortOrder" xsi:type="string">1</item>
                                            <item name="children" xsi:type="array"></item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
    </referenceBlock>
</body>

#Mike/Checkout/view/frontend/web/js/view/shipping-methods.js
(This is pretty much just a copy of Magento_Checkout/view/web/js/view/shipping.js to make sure I have all of methods I need)
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/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,
    shippingRatesValidator,
    formPopUpState,
    shippingService,
    selectShippingMethodAction,
    rateRegistry,
    setShippingInformationAction,
    stepNavigator,
    modal,
    checkoutDataResolver,
    checkoutData,
    registry,
    $t
) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Mike_Checkout/shipping-methods'
        },

        //add here your logic to display step,
        isVisible: ko.observable(true),
        visible: ko.observable(!quote.isVirtual()),
        errorValidationMessage: ko.observable(false),
        isFormInline: addressList().length == 0,

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

            stepNavigator.registerStep(
                //step code will be used as step content id in the component template
                'shipping_methods',
                //step alias
                null,
                //step title value
                'Shipping Methods',
                //observable property with logic when display step or hide step
                this.isVisible,

                _.bind(this.navigate, this),

                17
            );


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


        rates: shippingService.getShippingRates(),
        isLoading: shippingService.isLoading,

        /**
         * Set shipping information handler
         */
        setShippingInformation: function () {
            if (this.validateShippingInformation()) {
                setShippingInformationAction().done(
                    function () {
                        stepNavigator.next();
                    }
                );
            }
        },

        /**
         * @return {Boolean}
         */
        validateShippingInformation: function () {
            var shippingAddress,
                addressData,
                loginFormSelector = 'form[data-role=email-with-possible-login]',
                emailValidationResult = customer.isLoggedIn();

            if (!quote.shippingMethod()) {
                this.errorValidationMessage('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.source.trigger('shippingAddress.data.validate');

                if (this.source.get('shippingAddress.custom_attributes')) {
                    this.source.trigger('shippingAddress.custom_attributes.data.validate');
                }

                if (this.source.get('params.invalid') ||
                    !quote.shippingMethod().method_code ||
                    !quote.shippingMethod().carrier_code ||
                    !emailValidationResult
                ) {
                    return false;
                }

                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;
                }
                selectShippingAddress(shippingAddress);
            }

            if (!emailValidationResult) {
                $(loginFormSelector + ' input[name=username]').focus();

                return false;
            }

            return true;
        }


    });
}
);

#Mike/Checkout/view/frontend/web/template/shipping-methods.html

<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" data-bind="i18n: 'Shipping Methods'" data-role="title"></div>
    <!-- ko foreach: getRegion('before-shipping-method-form') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!-- /ko -->
    <div id="checkout-step-shipping_method"
         class="step-content"
         data-role="content"
         role="tabpanel"
         aria-hidden="false">
        <!-- ko if: rates().length  -->
        <form class="form methods-shipping" id="co-shipping-method-form" data-bind="submit: setShippingInformation" novalidate="novalidate">
            <div id="checkout-shipping-method-load">
                <table class="table-checkout-shipping-method">
                    <thead>
                        <tr class="row">
                            <th class="col col-method" data-bind="i18n: 'Select Method'"></th>
                            <th class="col col-price" data-bind="i18n: 'Price'"></th>
                            <th class="col col-method" data-bind="i18n: 'Method Title'"></th>
                            <th class="col col-carrier" data-bind="i18n: 'Carrier Title'"></th>
                        </tr>
                    </thead>
                    <tbody>

                    <!--ko foreach: { data: rates(), as: 'method'}-->
                    <tr class="row" data-bind="click: $parent.selectShippingMethod">
                        <td class="col col-method">
                            <!-- ko ifnot: method.error_message -->
                            <!-- ko if: $parent.rates().length == 1 -->
                            <input class="radio"
                                   type="radio"
                                   data-bind="attr: {
                                                checked: $parent.rates().length == 1,
                                                'value' : method.carrier_code + '_' + method.method_code,
                                                'id': 's_method_' + method.method_code,
                                                'aria-labelledby': 'label_method_' + method.method_code + '_' + method.carrier_code + ' ' + 'label_carrier_' + method.method_code + '_' + method.carrier_code
                                             }" />
                            <!-- /ko -->
                            <!--ko ifnot: ($parent.rates().length == 1)-->
                            <input type="radio"
                                   data-bind="
                                            value: method.carrier_code + '_' + method.method_code,
                                            checked: $parent.isSelected,
                                            attr: {
                                                'id': 's_method_' + method.carrier_code + '_' + method.method_code,
                                                'aria-labelledby': 'label_method_' + method.method_code + '_' + method.carrier_code + ' ' + 'label_carrier_' + method.method_code + '_' + method.carrier_code
                                            },
                                            click: $parent.selectShippingMethod"
                                   class="radio"/>
                            <!--/ko-->
                            <!-- /ko -->
                        </td>
                        <td class="col col-price">
                            <!-- ko foreach: $parent.getRegion('price') -->
                            <!-- ko template: getTemplate() --><!-- /ko -->
                            <!-- /ko -->
                        </td>

                        <td class="col col-method"
                                data-bind="text: method.method_title, attr: {'id': 'label_method_' + method.method_code + '_' + method.carrier_code}"></td>

                        <td class="col col-carrier"
                                data-bind="text: method.carrier_title, attr: {'id': 'label_carrier_' + method.method_code + '_' + method.carrier_code}"></td>
                    </tr>

                    <!-- ko if:  method.error_message -->
                    <tr class="row row-error">
                        <td class="col col-error" colspan="4">
                            <div class="message error">
                                <div data-bind="text: method.error_message"></div>
                            </div>
                            <span class="no-display">
                                <input type="radio" data-bind="attr: {'value' : method.method_code, 'id': 's_method_' + method.method_code}"/>
                            </span>
                        </td>
                    </tr>
                    <!-- /ko -->

                    <!-- /ko -->
                    </tbody>
                </table>
            </div>

            <div id="onepage-checkout-shipping-method-additional-load">
                <!-- ko foreach: getRegion('shippingAdditional') -->
                <!-- ko template: getTemplate() --><!-- /ko -->
                <!-- /ko -->
            </div>
            <!-- ko if: errorValidationMessage().length > 0 -->
            <div class="message notice">
                <span><!-- ko text: errorValidationMessage()--><!-- /ko --></span>
            </div>
            <!-- /ko -->
            <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><!-- ko i18n: 'Next'--><!-- /ko --></span>
                    </button>
                </div>
            </div>
        </form>
        <!-- /ko -->
        <!-- ko ifnot: rates().length > 0 --><div class="no-quotes-block"><!-- ko i18n: 'Sorry, no quotes are available for this order at this time'--><!-- /ko --></div><!-- /ko -->
    </div>
</div>
</li>

I have worked out that the template: $parent.getRegion('price') isn't being pulled in, in new shipping-methods.html file.

<td class="col col-price">
    <!-- ko foreach: $parent.getRegion('price') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!-- /ko -->
</td>

I'm guessing my module isn't able to access the templates it needs to generate the prices?

Update:
I've noticed that isDisplayShippingPriceExclTax is set to false in the window.checkoutConfig, though, this is true on the M2 demo site checkout. Any relation?

Also, from further research, it looks like the shipping method prices are generated via the Magento_Tax/template/checkout/shipping_method/price.html template. Why is this not available to my new module?

To summarise:
The shipping method prices aren't showing in my new module.

Best Answer

I got it working! I needed to included the Magento_Tax/js/view/checkout/shipping_method/price component (and display area) as a child of my new step module in my checkout_index_index.xml:

#app/code/Mike/Checkout/view/frontend/layout/checkout_index_index.xml

<item name="shipping-methods" xsi:type="array">
<item name="component" xsi:type="string">Mike_Checkout/js/view/shipping-methods</item>
<item name="sortOrder" xsi:type="string">1</item>
<item name="children" xsi:type="array">
    <item name="price" xsi:type="array">
        <item name="component" xsi:type="string">Magento_Tax/js/view/checkout/shipping_method/price</item>
        <item name="displayArea" xsi:type="string">price</item>
    </item>
</item>

Related Topic