Add Steps Before and After Shipping and Payment Review
- Shipping Information
- Review and Payment Information
Arsal/CheckoutSteps/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="Arsal_CheckoutSteps" setup_version="1.0.0"> <sequence> <module name="Magento_Checkout" /> </sequence> </module> </config>Register our module:
Arsal/CheckoutSteps/registration.php
<?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'Arsal_CheckoutSteps', __DIR__ );First we are going to create customer step which will allow only the logged in customer to place order. We will provide a login form for guest customer at this step.We will use js ui component to create our custom step. The code inside component will tell magento about our step.
Let’s go ahead and create checkout layout xml first. We will declare our component inside this layout which will push our custom step to the checkout page.Create a file checkout_index_index.xml inside our frontend layout:
Arsal/CheckoutSteps/view/frontend/layout/checkout_index_index.xml:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout.root"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="checkout" xsi:type="array"> <item name="children" xsi:type="array"> <item name="steps" xsi:type="array"> <item name="children" xsi:type="array"> <item name="customer-account-step" xsi:type="array"> <item name="component" xsi:type="string">uiComponent</item> <item name="sortOrder" xsi:type="string">1</item> <item name="children" xsi:type="array"> <item name="customerAccount" xsi:type="array"> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/customer-step</item> <item name="provider" xsi:type="string">checkoutProvider</item> <item name="sortOrder" xsi:type="string">1</item> <item name="children" xsi:type="array"> <!-- your login form goes here --> </item> </item> </item> </item> <item name="shipping-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">2</item> </item> <item name="billing-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">3</item> </item> </item> </item> </item> </item> </item> </argument> </arguments> </referenceBlock> </body> </page>We have just declare our component inside layout. Now to create and display our custom step we need to create this component inside our web directory. Create custom-step.js
Arsal_CheckoutSteps/view/frontend/web/js/view/customer-step.js
define(
[
'jquery',
'ko',
'uiComponent',
'underscore',
'Magento_Checkout/js/model/step-navigator',
'Magento_Customer/js/model/customer',
'mage/translate'
],
function (
$,
ko,
Component,
_,
stepNavigator,
customer,
$t
) {
'use strict';
return Component.extend({
defaults: {
template: 'Arsal_CheckoutSteps/customerstep'
},
isVisible: ko.observable(false),
customerTitle: 'Customer Login',
/**
*
* @returns {*}
*/
initialize: function () {
this._super();
if (!customer.isLoggedIn()) {
this.isVisible(true);
stepNavigator.registerStep(
'customer-step',
null,
$t('Customer'),
this.isVisible,
_.bind(this.navigate, this),
8
);
} else {
this.isVisible(false);
}
return this;
},
navigate: function() {},
/**
* @returns void
*/
navigateToNextStep: function () {
if (!customer.isLoggedIn()) {
stepNavigator.next();
}
}
});
}
);
Inside the component customer-step we have told magento to register our custom step only when customer is logged in to the system.Now create renderer template of the component. Create customerstep.html template file inside your web directory.
Arsal/CheckoutSteps/view/frontend/web/template/customerstep.html
<li id="customer-step" data-bind="fadeVisible: isVisible"> <div class="step-title" data-bind="i18n: customerTitle" data-role="title"></div> <div id="customer-step-title" class="step-content" data-role="content"> <form data-bind="submit: navigateToNextStep" novalidate="novalidate" class="form form-shipping-address"> <fieldset class="fieldset"> <!-- ko foreach: getRegion('customer-fields') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> </fieldset> </form> </div> </li>Our first step has been created with empty content:
Remember: The template already contains the region renderer with name “customer-fields”. All we have to do is to modify checkout_index_index.xml:After modification our checkout_index_index.xml looks like now:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout.root"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="checkout" xsi:type="array"> <item name="children" xsi:type="array"> <item name="steps" xsi:type="array"> <item name="children" xsi:type="array"> <item name="customer-account-step" xsi:type="array"> <item name="component" xsi:type="string">uiComponent</item> <item name="sortOrder" xsi:type="string">1</item> <item name="children" xsi:type="array"> <item name="customerAccount" xsi:type="array"> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/customer-step</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="customer-fields" xsi:type="array"> <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" /> </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"> <!-- Add your custom components here --> </item> </item> </item> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/account</item> <item name="displayArea" xsi:type="string">customer-fields</item> <item name="tooltip" xsi:type="array"> <item name="description" xsi:type="string">Enter your email associated with store.</item> </item> </item> </item> </item> </item> </item> <item name="shipping-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">2</item> </item> <item name="billing-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">3</item> </item> </item> </item> </item> </item> </item> </argument> </arguments> </referenceBlock> </body> </page>According to above layout we need to add another component which will be responsible for creating a login form for us.
Create account.js component inside your web directory:
Arsal/CheckoutSteps/view/frontend/web/js/view/account.js
define(
[
'jquery',
'uiComponent',
'ko',
'Magento_Customer/js/model/customer',
'Magento_Customer/js/action/login',
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/checkout-data',
'Magento_Checkout/js/model/full-screen-loader',
'mage/validation'
], function (
$,
Component,
ko,
customer,
loginAction,
quote,
checkoutData,
fullScreenLoader
) {
'use strict';
return Component.extend({
defaults: {
template: 'Arsal_CheckoutSteps/form/account',
email: checkoutData.getInputFieldEmailValue(),
},
emailFocused: false,
forgotPasswordUrl: window.checkoutConfig.forgotPasswordUrl,
createNewAccountUrl: window.checkoutConfig.registerUrl,
/**
* Initializes observable properties of instance
*
* @returns {Object} Chainable.
*/
initObservable: function () {
this._super()
.observe(['email','emailFocused']);
return this;
},
/**
* Log in form submitting callback.
*
* @param {HTMLElement} loginForm - form element.
*/
login: function (loginForm) {
var loginData = {},
formDataArray = $(loginForm).serializeArray();
formDataArray.forEach(function (entry) {
loginData[entry.name] = entry.value;
});
if ($(loginForm).validation() && $(loginForm).validation('isValid')) {
fullScreenLoader.startLoader();
loginAction(loginData, window.checkoutConfig.checkoutUrl).always(function () {
fullScreenLoader.stopLoader();
});
}
}
});
}
);
Now create its renderer template:Arsal/CheckoutSteps/view/frontend/web/template/form/account.html
<!-- ko foreach: getRegion('before-login-form') --> <!-- ko template: getTemplate() --><!-- /ko --> <!-- /ko --> <form class="form form-login" data-bind="submit:login" method="post"> <fieldset id="customer-account-fieldset" class="fieldset"> <div class="field required"> <label class="label" for="customer-email"> <span data-bind="i18n: 'Email Address'"></span> </label> <div class="control _with-tooltip"> <input class="input-text" type="email" data-bind=" textInput: email, hasFocus: emailFocused, mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" id="customer-email" /> <!-- ko template: 'ui/form/element/helper/tooltip' --><!-- /ko --> </div> </div> <div class="field"> <label class="label" for="customer-password"> <span data-bind="i18n: 'Password'"></span> </label> <div class="control"> <input class="input-text" data-bind=" attr: { placeholder: $t('Password'), }" type="password" name="password" id="customer-password" data-validate="{required:true}" autocomplete="off"/> <span class="note" data-bind="i18n: 'Sign in to continue.'"></span> </div> </div> <!-- ko foreach: getRegion('additional-login-form-fields') --> <!-- ko template: getTemplate() --><!-- /ko --> <!-- /ko --> <div class="actions-toolbar"> <input name="context" type="hidden" value="checkout" /> <div class="primary"> <button type="submit" class="action login primary" data-action="checkout-method-login"><span data-bind="i18n: 'Login'"></span></button> </div> <div class="secondary"> <a class="action remind" data-bind="attr: { href: forgotPasswordUrl }"> <span data-bind="i18n: 'Forgot Your Password?'"></span> </a> <a class="action create" data-bind="attr: { href: createNewAccountUrl }"> <span data-bind="i18n: 'Create Your Account'"></span> </a> </div> </div> </fieldset> </form>Now our Layout should look like so:
Now try loging into your account via these field, this will log you into your account and will auto refresh to display changes. The loaded page exlude this step as per our logic and will directly display shipping step.Congragulations! you’ve completed your first objective. Now let’s keep going and add another step to our checkout to enhance the page.
Let’s modify checkout_index_index.xml again by adding our success component along with sort order:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout.root"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="checkout" xsi:type="array"> <item name="children" xsi:type="array"> <item name="steps" xsi:type="array"> <item name="children" xsi:type="array"> <item name="customer-account-step" xsi:type="array"> <item name="component" xsi:type="string">uiComponent</item> <item name="sortOrder" xsi:type="string">1</item> <item name="children" xsi:type="array"> <item name="customerAccount" xsi:type="array"> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/customer-step</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="customer-fields" xsi:type="array"> <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" /> </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"> <!-- Add your custom components here --> </item> </item> </item> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/account</item> <item name="displayArea" xsi:type="string">customer-fields</item> <item name="tooltip" xsi:type="array"> <item name="description" xsi:type="string">Enter your email associated with store.</item> </item> </item> </item> </item> </item> </item> <item name="shipping-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">2</item> </item> <item name="billing-step" xsi:type="array"> <item name="sortOrder" xsi:type="string">3</item> </item> <item name="success" xsi:type="array"> <item name="component" xsi:type="string">uiComponent</item> <item name="sortOrder" xsi:type="string">4</item> <item name="children" xsi:type="array"> <item name="orderSuccess" xsi:type="array"> <item name="component" xsi:type="string">Arsal_CheckoutSteps/js/view/success-step</item> <item name="provider" xsi:type="string">checkoutProvider</item> <item name="sortOrder" xsi:type="string">1</item> <item name="config" xsi:type="array"> <item name="template" xsi:type="string">Arsal_CheckoutSteps/successStep</item> <item name="title" xsi:type="string">Thank you for your purchase!</item> </item> <item name="children" xsi:type="array" /> </item> </item> </item> </item> </item> </item> </item> </item> </argument> </arguments> </referenceBlock> </body> </page>As per layout we need to create component success-step and template successStep
Let’s create our component first:
Arsal/CheckoutSteps/view/frontend/web/js/view/success-step.js
define(
[
'jquery',
'ko',
'uiComponent',
'underscore',
'Magento_Checkout/js/model/step-navigator',
'Magento_Checkout/js/model/quote',
'Magento_Customer/js/model/customer',
'mage/translate',
'mage/url',
'Magento_Checkout/js/model/full-screen-loader'
],
function (
$,
ko,
Component,
_,
stepNavigator,
quote,
customer,
$t,
url,
fullScreenLoader
) {
'use strict';
return Component.extend({
isVisible: ko.observable(true),
title: 'Success Page',
initialize: function (config) {
this._super();
stepNavigator.registerStep(
'success-step',
null,
$t('Success'),
this.isVisible,
_.bind(this.navigate, this),
22
);
if (config.title !== undefined) {
this.title = config.title;
}
return this;
},
navigate: function() {},
/**
* @returns void
*/
navigateToNextStep: function () {
fullScreenLoader.startLoader();
window.location.replace(url.build(''));
},
getOrderText: function () {
return 'Your order has been placed Successfully. Click Continue to proceed with shopping'
},
getOrderViewUrl: function () {
return url.build('sales/order/history');
}
});
}
);
Create template file successStepArsal/CheckoutSteps/view/frontend/web/template/successStep.html
<li id="customer-step" data-bind="fadeVisible: isVisible"> <div class="step-title" data-bind="i18n: title" data-role="title"></div> <div id="success-step-title" class="step-content" data-role="content"> <form data-bind="submit: navigateToNextStep" novalidate="novalidate" class="form"> <fieldset class="fieldset"> <div class="checkout-success"> <p data-bind="text: getOrderText()"></p> <div class="view-orders"> <a class="action view-orders" data-bind="attr: { href: getOrderViewUrl() }, text: 'View Your Orders'"></a> </div> </div> </fieldset> <div class="actions-toolbar"> <div class="primary"> <button data-role="opc-continue" type="submit" class="button action continue primary"> <span><!-- ko i18n: 'Continue Shopping'--><!-- /ko --></span> </button> </div> </div> </form> </div> </li>
This arises the difficulty level. To consider solution to this we have multiple options, like:
- Extend default.js component present in Magento_Checkout/js/view/payment/default.js and add our logic to afterPlaceOrder method.
- Extend / customize Magento_Checkout/js/action/redirect-on-success.js action and add custom logic to deal with navigator.
Arsal/CheckoutSteps/view/frontend/requirejs-config.js
var config = {
map: {
'*' : {
'Magento_Checkout/js/action/redirect-on-success':
'Arsal_CheckoutSteps/js/action/redirect-on-success'
}
}
};
Now according to our requirejs-config create an action js file:Arsal/CheckoutSteps/view/frontend/web/js/action/redirect-on-success
define(
[
'Magento_Checkout/js/model/step-navigator'
],
function (stepNavigator) {
'use strict';
return {
execute: function () {
stepNavigator.next();
}
}
}
);
This action file is great help to us and thus prove best in redirecting magento to our next step.Arsal/CheckoutSteps/etc/frontend/routes.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router id="standard"> <route id="checkoutsteps" frontName="checkoutsteps"> <module name="Arsal_CheckoutSteps" /> </route> </router> </config>Now create a controller “Success”:
Arsal/CheckoutSteps/Controller/Index/Success.php
<?php namespace Arsal\CheckoutSteps\Controller\Index; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Checkout\Model\Session; class Success extends Action { /** * @var JsonFactory $resultJsonFactory */ protected $resultJsonFactory; /** * @var Session $checkoutSession */ private $checkoutSession; /** * @var OrderRepositoryInterface $orderRepository */ private $orderRepository; /** * Success constructor. * @param Context $context * @param JsonFactory $resultJsonFactory * @param Session $session * @param OrderRepositoryInterface $orderRepository */ public function __construct( Context $context, JsonFactory $resultJsonFactory, Session $session, OrderRepositoryInterface $orderRepository ) { parent::__construct($context); $this->resultJsonFactory = $resultJsonFactory; $this->checkoutSession = $session; $this->orderRepository = $orderRepository; } public function execute() { /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); $orderId = $this->checkoutSession->getData('last_order_id'); $response = []; if (!empty($orderId)) { $order = $this->orderRepository->get($orderId); $items = []; foreach ($order->getItems() as $item) { $items[] = $item->getData(); } $response = [ 'orderId' => $order->getIncrementId(), 'orderItems' => $items, 'shippingAddress' => $order->getShippingAddress()->getData(), 'billingAddress' => $order->getBillingAddress()->getData(), 'grandTotal' => $order->getBaseGrandTotal(), 'shippingTotal' => $order->getBaseShippingAmount(), ]; } $resultJson->setData($response); return $resultJson; } }Now open up your component “success-step.js” and update with the following code:
Arsal/CheckoutSteps/view/frontend/web/js/view/success-step.js
define(
[
'jquery',
'ko',
'uiComponent',
'underscore',
'Magento_Checkout/js/model/step-navigator',
'Magento_Checkout/js/model/quote',
'Magento_Catalog/js/price-utils',
'Magento_Customer/js/model/customer',
'mage/translate',
'mage/url',
'Magento_Checkout/js/model/full-screen-loader',
'mage/storage'
],
function (
$,
ko,
Component,
_,
stepNavigator,
quote,
priceUtils,
customer,
$t,
url,
fullScreenLoader,
storage
) {
'use strict';
return Component.extend({
isVisible: ko.observable(false),
order: ko.observable([]),
displayOrderInfo: ko.observable(false),
title: 'Success Page',
orderId: ko.observable(),
shippingAddress: ko.observable(false),
initialize: function (config) {
this._super();
var self = this;
this.isVisible.subscribe(function (value) {
if (value) {
fullScreenLoader.startLoader();
storage.post(
url.build('checkoutsteps/index/success'), JSON.stringify([])
).always(
function (response) {
fullScreenLoader.stopLoader();
if (response && response.orderId !== undefined) {
self.order(response);
self.orderId(response.orderId);
self.displayOrderInfo(true);
var customerData = window.checkoutConfig.customerData;
if (
customerData !== undefined
&& customerData.addresses !== undefined
&& customerData.addresses[0] !== undefined
&& customerData.addresses[0].length
&& customerData.addresses[0].inline !== undefined
) {
self.shippingAddress(customerData.addresses[0].inline);
}
else {
self.__setAddress(response.shippingAddress, self);
}
}
}
)
}
});
stepNavigator.registerStep(
'success-step',
null,
$t('Success'),
this.isVisible,
_.bind(this.navigate, this),
22
);
if (config.title !== undefined) {
this.title = config.title;
}
return this;
},
navigate: function() {},
/**
* @returns void
*/
navigateToNextStep: function () {
fullScreenLoader.startLoader();
window.location.replace(url.build(''));
},
getOrderText: function () {
return 'Your order has been placed Successfully.'
},
getOrderViewUrl: function () {
return url.build('sales/order/history');
},
getQuoteItems: function() {
return window.checkoutConfig.quoteItemData;
},
getTotalsData: function() {
return window.checkoutConfig.totalsData.total_segments;
},
getFormattedPrice: function (price) {
return priceUtils.formatPrice(price, quote.getPriceFormat());
},
__setAddress: function ($addressObj, instance) {
if (instance === undefined) {
instance = this;
}
var addressString = '';
addressString += $addressObj.firstname + ' ' + $addressObj.lastname + ', ';
addressString += $addressObj.street + ', ';
if ($addressObj.city !== undefined) {
addressString = $addressObj.city + ', ';
}
if ($addressObj.region !== undefined) {
addressString += $addressObj.region + ' ';
}
if ($addressObj.postCode !== undefined) {
addressString += $addressObj.postCode + ' ';
}
addressString += $addressObj.country_id;
if ($addressObj.address_type === 'shipping') {
instance.shippingAddress(addressString);
}
}
});
}
);
Now update your template “successStep” accordingly: Arsal/CheckoutSteps/view/frontend/web/template/successStep.html
<li id="customer-step" data-bind="fadeVisible: isVisible"> <div class="step-title" data-bind="i18n: title" data-role="title"></div> <div id="success-step-title" class="step-content" data-role="content"> <form data-bind="submit: navigateToNextStep" novalidate="novalidate" class="form"> <fieldset class="fieldset"> <div class="checkout-success"> <p data-bind="text: getOrderText()"></p> <div data-bind="fadeVisible: displayOrderInfo, attr: { class: 'order-information' }"> <p>Your Order Id is: #<strong data-bind="text: orderId"></strong></p> <div class="address-section"> <div class="shipping-address" data-bind="fadeVisible: shippingAddress"> <h3 data-bind="text: 'Shipping Address'"></h3> <p data-bind="text: shippingAddress"></p> </div> </div> <div class="product-information"> <h3 data-bind="text: 'Product Information'"></h3> <ul data-bind="foreach: getQuoteItems()"> <li> <ul> <li> <img data-bind="attr: {src: thumbnail, alt: name, title: name}" /> </li> <li> <div data-bind="text: name"></div> <div class="sku"> SKU: <span data-bind="text: sku"></span> </div> <div class="qty"> Qty: <span data-bind="text: qty"></span> </div> <div class="Price"> Price: <span data-bind="text: $parent.getFormattedPrice(price)"></span> </div> <div class="tax-amount"> Tax: <span data-bind="text: $parent.getFormattedPrice(tax_amount)"></span> </div> <div class="row-total"> Row Total: <span data-bind="text: $parent.getFormattedPrice(row_total_incl_tax)"></span> </div> </li> </ul> </li> </ul> </div> <div class="order-summary"> <ul data-bind="foreach: getTotalsData()"> <li> <span data-bind="text: title"></span> <span data-bind="text: $parent.getFormattedPrice(value)"></span> </li> </ul> </div> </div> <div class="view-orders"> <a class="action view-orders" data-bind="attr: { href: getOrderViewUrl() }, text: 'View Your Orders'"></a> </div> </div> </fieldset> <div class="actions-toolbar"> <div class="primary"> <button data-role="opc-continue" type="submit" class="button action continue primary"> <span><!-- ko i18n: 'Continue Shopping'--><!-- /ko --></span> </button> </div> </div> </form> </div> </li>The module is available on github:
https://github.com/arsalanworld/checkout-steps
8 Comments
Wonderful post! We will be linking to this great post on our site. Keep up the good writing.
Thank you. Sure you can link it.
I dugg some of you post as I thought they were very useful very beneficial
thank you, I am glad you found the posts helpful.
Awesome post! Keep up the great work! 🙂
Thank you for encouragement. 🙂
Wow great. Thanks alot for this. Appreciated!
Thanks.