Magento Add Checkout Step

Adding a custom checkout step to Magento’s Onepage checkout is no easy task. It’s important to understand the templates and block classes involved, as well as the JavaScript used to show and hide the relevant steps.

Let’s take a look at the RWD checkout and outline what is already defined.

Firstly, we have six checkout steps: Login/Register, Billing Information, Shipping Information, Shipping Method, Payment Information and Order Review.

The template files that are used for each checkout steps are as follows:

  • Login/Register – frontend/rwd/default/template/persistent/checkout/onepage/login.phtml.
  • Billing Information – frontend/rwd/default/template/persistent/checkout/onepage/billing.phtml.
  • Shipping Information – frontend/rwd/default/template/checkout/onepage/shipping.phtml.
  • Shipping Method – frontend/base/default/template/checkout/onepage/shipping_method.phtml.
  • Payment Information – frontend/rwd/default/template/checkout/onepage/payment.phtml.
  • Order Review – frontend/base/default/template/checkout/onepage/review.phtml.

These templates are all rendered within a wrapper template file, frontend/rwd/default/template/checkout/onepage.phtml.

Let’s take a look at these templates declared within the checkout.xml layout file and determine what block type they are using.

<checkout_onepage_index>
    ....
    <reference name="content">
        <block type="checkout/onepage" name="checkout.onepage" template="checkout/onepage.phtml">
            <block type="checkout/onepage_login" name="checkout.onepage.login" as="login" template="checkout/onepage/login.phtml">
                <block type="page/html_wrapper" name="checkout.onepage.login.before" as="login_before" translate="label">
                    <label>Login/Registration Before</label>
                    <action method="setMayBeInvisible"><value>1</value></action>
                </block>
            </block>
            <block type="checkout/onepage_billing" name="checkout.onepage.billing" as="billing" template="checkout/onepage/billing.phtml"/>
            <block type="checkout/onepage_shipping" name="checkout.onepage.shipping" as="shipping" template="checkout/onepage/shipping.phtml"/>
            <block type="checkout/onepage_shipping_method" name="checkout.onepage.shipping_method" as="shipping_method" template="checkout/onepage/shipping_method.phtml">
                <block type="checkout/onepage_shipping_method_available" name="checkout.onepage.shipping_method.available" as="available" template="checkout/onepage/shipping_method/available.phtml"/>
                <block type="checkout/onepage_shipping_method_additional" name="checkout.onepage.shipping_method.additional" as="additional" template="checkout/onepage/shipping_method/additional.phtml"/>
            </block>
            <block type="checkout/onepage_payment" name="checkout.onepage.payment" as="payment" template="checkout/onepage/payment.phtml">
                <block type="checkout/onepage_payment_methods" name="checkout.payment.methods" as="methods" template="checkout/onepage/payment/info.phtml">
                    <action method="setMethodFormTemplate"><method>purchaseorder</method><template>payment/form/purchaseorder.phtml</template></action>
                </block>
            </block>
            <block type="checkout/onepage_review" name="checkout.onepage.review" as="review" template="checkout/onepage/review.phtml"/>
        </block>
    </reference>
</checkout_onepage_index>

So the block types used for the checkout steps can be seen below.

  • Checkout wrapper – checkout/onepageMage/Checkout/Block/Onepage.php.
  • Login/Register – checkout/onepage_loginMage/Checkout/Block/Onepage/Login.php.
  • Billing Information – checkout/onepage_billingMage/Checkout/Block/Onepage/Billing.php.
  • Shipping Information – checkout/onepage_shippingMage/Checkout/Block/Onepage/Shipping.php.
  • Shipping Method – checkout/onepage_shipping_methodMage/Checkout/Block/Onepage/Shipping/Method.php.
  • Payment Information – checkout/onepage_paymentMage/Checkout/Block/Onepage/Payment.php.
  • Order Review – checkout/onepage_reviewMage/Checkout/Block/Onepage/Review.php.

The JavaScript files used for the checkout are defined at the top of the onepage wrapper template file.

<div class="page-title">
    <h1><?php echo $this->__('Checkout') ?></h1>
</div>
<script type="text/javascript" src="<?php echo $this->getJsUrl('varien/accordion.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout_rwd.js') ?>"></script>

Let’s firstly take a look at the Onepage.php block file which is the block of the wrapper template file. We can see this a getSteps() method.

public function getSteps()
{
    $steps = array();
    $stepCodes = $this->_getStepCodes();

    if ($this-&gt;isCustomerLoggedIn()) {
        $stepCodes = array_diff($stepCodes, array('login'));
    }

    foreach ($stepCodes as $step) {
        $steps[$step] = $this->getCheckout()->getStepData($step);
    }

    return $steps;
}

If we then take a look at the _getStepCodes() method within the Mage_Checkout_Block_Onepage_Abstract class.

protected function _getStepCodes()
{
    return array('login', 'billing', 'shipping', 'shipping_method', 'payment', 'review');
}

Aha! So we’re starting to see how Magento defines its initial checkout steps. You may have noticed there is another reference in the opcheckout.js file that also defines the checkout steps.

var Checkout = Class.create();
Checkout.prototype = {
    initialize: function(accordion, urls){
        this.accordion = accordion;
        this.progressUrl = urls.progress;
        this.reviewUrl = urls.review;
        this.saveMethodUrl = urls.saveMethod;
        this.failureUrl = urls.failure;
        this.billingForm = false;
        this.shippingForm= false;
        this.syncBillingShipping = false;
        this.method = '';
        this.payment = '';
        this.loadWaiting = false;
        this.steps = ['login', 'billing', 'shipping', 'shipping_method', 'payment', 'review'];
        //We use billing as beginning step since progress bar tracks from billing
        this.currentStep = 'billing';

        this.accordion.sections.each(function(section) {
            Event.observe($(section).down('.step-title'), 'click', this._onSectionClick.bindAsEventListener(this));
        }.bind(this));

        this.accordion.disallowAccessToNextSections = true;
    },

So now that we’ve seen the basic concepts of the Magento checkout, let’s add a checkout step that will appear after the Login/Register step but before the Billing Information Step.

The checkout step will be added to a custom theme that is a child of the rwd theme.

Let’s start with defining our own module:

<?xml version="1.0"?>
<config>
    <modules>
        <Siphor_Customcheckout>
            <codePool>local</codePool>
            <active>true</active>
        </Siphor_Customcheckout>
    </modules>
</config>

Each checkout step within Magento has its own block class, therefore we’ll need to add our own one.

Create a config.xml configuration file and add the block prefix within the pair of <block> nodes.

<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <customcheckout>
                <class>Siphor_Customcheckout_Block</class>
            </customcheckout>
        </blocks>
    </global>
</config>

Then add the checkout step block.

<?php
class Siphor_Customcheckout_Block_Checkout_Onepage_Customstep extends Mage_Checkout_Block_Onepage_Abstract
{
    protected function _construct()
    {
        $this-&gt;getCheckout()->setStepData('customstep', array(
            'label'     => Mage::helper('checkout')->__('Custom Step'),
            'is_show'   => $this->isShow()
        ));

        if ($this->isCustomerLoggedIn()) {
            $this->getCheckout()->setStepData('customstep', 'allow', true);
            $this->getCheckout()->setStepData('billing', 'allow', false);
        }

        parent::_construct();
    }
}

As seen earlier, the _getStepCodes() method needs to be overridden within the Onepage.php block class in order for the additional step to be added into the array.

Therefore we need to start by adding the rewrite configuration within config.xml.

<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <customcheckout>
                <class>Siphor_Customcheckout_Block</class>
            </customcheckout>
            <checkout>
                <rewrite>
                    <onepage>Siphor_Customcheckout_Block_Checkout_Onepage</onepage>
                </rewrite>
            </checkout>
        </blocks>
    </global>
</config>

Then the block class can be added with the custom checkout step code added in.

<?php
class Siphor_Customcheckout_Block_Checkout_Onepage extends Mage_Checkout_Block_Onepage
{
    public function getSteps()
    {
        $steps = array();

        if (!$this->isCustomerLoggedIn()) {
            $steps['login'] = $this->getCheckout()->getStepData('login');
        }

        // New code
        $stepCodes = array('customstep','billing', 'shipping', 'shipping_method', 'payment', 'review');

        foreach ($stepCodes as $step) {
            $steps[$step] = $this->getCheckout()->getStepData($step);
        }
        return $steps;
    }

    public function getActiveStep()
    {
        // If the user is already logged in, go to the new 'customstep' step
        return $this->isCustomerLoggedIn() ? 'customstep' : 'login';
    }

}

Magento places the opcheckout.js within the onepage.phtml file and not through layout XML, so a custom onepage.phtml will need to used to add in the additional JavaScript file that will include the additional checkout step.

We can specify a frontend layout file within the config.xml file.

<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <customcheckout>
                <class>Siphor_Customcheckout_Block</class>
            </customcheckout>
            <checkout>
                <rewrite>
                    <onepage>Siphor_Customcheckout_Block_Checkout_Onepage</onepage>
                </rewrite>
            </checkout>
        </blocks>
    </global>
    <frontend>
        <layout>
            <updates>
                <customcheckout>
                    <file>customcheckout.xml</file>
                </customcheckout>
            </updates>
        </layout>
    </frontend>
</config>

Within the customcheckout.xml layout file, override the template of the onepage.phtml file to use our custom onepage.phtml and also include the template for our additional checkout step.

<?xml version="1.0"?>
<layout>
    <checkout_onepage_index>
        <reference name="checkout.onepage">
            <action method="setTemplate">
                <template>customcheckout/onepage.phtml</template>
            </action>
            <block type="customcheckout/checkout_onepage_customstep" name="customstep"
                   template="customcheckout/onepage/customstep.phtml" />
        </reference>
    </checkout_onepage_index>
</layout>

Note that the block name given to the checkout step in the layout file must match the step code specified within the blocks added above.

As there is an array of step codes present in the Checkout Javascript, we need to override this by adding a custom JavaScript file to onepage.phtml.

<?php
<div class="page-title">
    <h1><?php echo $this->__('Checkout') ?></h1>
</div>
<script type="text/javascript" src="<?php echo $this->getJsUrl('varien/accordion.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout_rwd.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('customcheckout/js/customcheckout.js') ?>"></script>

....

In the JavaScript file, add in the new checkout step code.

var Customcheckout = Class.create(Checkout, {
    initialize: function($super,accordion, urls){
        $super(accordion, urls);

        // New checkout step added
        this.steps = ['login', 'customstep' ,'billing', 'shipping', 'shipping_method', 'payment', 'review'];
    },
    setMethod: function(){
        if ($('login:guest') && $('login:guest').checked) {
            this.method = 'guest';
            var request = new Ajax.Request(
                this.saveMethodUrl,
                {method: 'post', onFailure: this.ajaxFailure.bind(this), parameters: {method:'guest'}}
            );
            Element.hide('register-customer-password');
            this.gotoSection('customstep');
        }

        //

        else if($('login:register') && ($('login:register').checked || $('login:register').type == 'hidden')) {
            this.method = 'register';
            var request = new Ajax.Request(
                this.saveMethodUrl,
                {method: 'post', onFailure: this.ajaxFailure.bind(this), parameters: {method:'register'}}
            );
            Element.show('register-customer-password');
            this.gotoSection('customstep');
        }
        else{
            alert(Translator.translate('Please choose to register or to checkout as a guest'));
            return false;
        }
    }
});

Within the onepage.phtml near the bottom, the default Checkout class is still being initialised. This will now need to change to the Customcheckout class.

<?php
<div class="page-title">
    <h1><?php echo $this->__('Checkout') ?></h1>
</div>
<script type="text/javascript" src="<?php echo $this->getJsUrl('varien/accordion.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('js/opcheckout_rwd.js') ?>"></script>
<script type="text/javascript" src="<?php echo $this->getSkinUrl('customcheckout/js/customcheckout.js') ?>"></script>

....

<script type="text/javascript">
    //<![CDATA[
    var accordion = new Accordion('checkoutSteps', '.step-title', true);
    <?php if($this->getActiveStep()): ?>
    accordion.openSection('opc-<?php echo $this->getActiveStep() ?>');
    <?php endif ?>
    var checkout = new Customcheckout(accordion,{
        progress: '<?php echo $this->getUrl('checkout/onepage/progress') ?>',
        review: '<?php echo $this->getUrl('checkout/onepage/review') ?>',
        saveMethod: '<?php echo $this->getUrl('checkout/onepage/saveMethod') ?>',
        failure: '<?php echo $this->getUrl('checkout/cart') ?>'}
    );
    //]]>
</script>

With the layout file we specified that the custom checkout step template would be added within the customcheckout/onepage directory. Let’s add this now.

<form id="customstep-form" action="">
    <fieldset>
        <ul class="form-list">
            <li id="customstep">
                <fieldset>
                    <ul>
                        <li class="wide">
                            <label for="customstep"><?php echo $this->__('Welcome to your new step!') ?></label>
                        </li>
                    </ul>
                </fieldset>
            </li>
        </ul>
        <div class="buttons-set" id="customstep-buttons-container">
            <button type="button" title="<?php echo $this->__('Continue') ?>" class="button" onclick="customstep.save()"><span><span><?php echo $this->__('Continue') ?></span></span></button>
            <span class="please-wait" id="customstep-please-wait" style="display:none;">
            <img src="<?php echo $this->getSkinUrl('images/opc-ajax-loader.gif') ?>" alt="<?php echo $this->__('Loading next step...') ?>" title="<?php echo $this->__('Loading next step...') ?>" class="v-middle" /> <?php echo $this->__('Loading next step...') ?>
        </span>
        </div>
    </fieldset>
</form>
<script type="text/javascript">
    //<![CDATA[
    var customstep = new CustomStep('customstep-form','<?php echo $this->getUrl('checkout/onepage/saveCustomStep') ?>');
    var customstepForm = new VarienForm('customstep-form');
    //]]>
</script>

For now this step will simply contain a label and a continue button without having to save any complex data.

As seen at the bottom of the file, a CustomStep JavaScript class needs to be added within customcheckout.js.

This class is used for validating the custom step and allowing the user to continue to the next step of the checkout.

var Customcheckout = Class.create(Checkout, {
    
    ....

}

var CustomStep = Class.create();
CustomStep.prototype = {
    initialize: function(form, saveUrl){
        this.form = form;
        if ($(this.form)) {
            $(this.form).observe('submit', function(event){this.save();Event.stop(event);}.bind(this));
        }
        this.saveUrl = saveUrl;
        this.validator = new Validation(this.form);
        this.onSave = this.nextStep.bindAsEventListener(this);
        this.onComplete = this.resetLoadWaiting.bindAsEventListener(this);
    },

    validate: function() {
        if(!this.validator.validate()) {
            return false;
        }
        return true;
    },

    save: function(){

        if (checkout.loadWaiting!=false) return;
        if (this.validate()) {
            checkout.setLoadWaiting('customstep');
            var request = new Ajax.Request(
                this.saveUrl,
                {
                    method:'post',
                    onComplete: this.onComplete,
                    onSuccess: this.onSave,
                    onFailure: checkout.ajaxFailure.bind(checkout),
                    parameters: Form.serialize(this.form)
                }
            );
        }
    },

    resetLoadWaiting: function(transport){
        checkout.setLoadWaiting(false);
    },

    nextStep: function(transport){
        if (transport && transport.responseText){
            try{
                response = eval('(' + transport.responseText + ')');
            }
            catch (e) {
                response = {};
            }
        }

        if (response.error) {
            alert(response.message);
            return false;
        }

        if (response.update_section) {
            $('checkout-'+response.update_section.name+'-load').update(response.update_section.html);
        }


        if (response.goto_section) {
            checkout.gotoSection(response.goto_section);
            checkout.reloadProgressBlock();
            return;
        }

        checkout.setBilling();
    }
}

The save checkout step URL that was specified in the customstep.phtml file needs to be added into the module.

Therefore within the config.xml file the Magento Checkout module’s OnepageController.php needs to be extended.

<?xml version="1.0"?>
<config>
    <frontend>
        ....
        <routers>
            <checkout>
                <args>
                    <modules>
                        <siphor_customcheckout before="Mage_Checkout">Siphor_Customcheckout</siphor_customcheckout>
                    </modules>
                </args>
            </checkout>
        </routers>
    </frontend>
    <global>
        ....
    </global>
</config>

The method within the controller would usually save the step data to the quote object. However as we’re simply displaying a label on the step, there is no need to do this.

<?php
require_once 'Mage/Checkout/controllers/OnepageController.php';
class Siphor_Customcheckout_OnepageController extends Mage_Checkout_OnepageController
{
    public function saveCustomStepAction(){
        if ($this->_expireAjax()) {
            return;
        }

        $result = array();
        $result['goto_section'] = 'billing';

        $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));

    }
}

And finally, we need to address an issue located in the opcheckout_rwd.js file which contains the following code.

Checkout.prototype.gotoSection = function (section, reloadProgressBlock) {
    // Adds class so that the page can be styled to only show the "Checkout Method" step
    if ((this.currentStep == 'login' || this.currentStep == 'billing') && section == 'billing') {
        $j('body').addClass('opc-has-progressed-from-login');
    }

    if (reloadProgressBlock) {
        this.reloadProgressBlock(this.currentStep);
    }
    this.currentStep = section;
    var sectionElement = $('opc-' + section);
    sectionElement.addClassName('allow');
    this.accordion.openSection('opc-' + section);

    // Scroll viewport to top of checkout steps for smaller viewports
    if (Modernizr.mq('(max-width: ' + bp.xsmall + 'px)')) {
        $j('html,body').animate({scrollTop: $j('#checkoutSteps').offset().top}, 800);
    }

    if (!reloadProgressBlock) {
        this.resetPreviousSteps();
    }
}

The important note to take from here is that a opc-has-progressed-from-login class gets added to the <body> element of the webpage.

Magento doesn’t seem to cater very well when adding a custom checkout step, and so this class never gets added as the assumed next step is the billing step.

To solve this, the gotoSection function can be copied from within this file into the Checkout class within the customcheckout.js file.

var Customcheckout = Class.create(Checkout, {
    
    ....

    gotoSection: function (section, reloadProgressBlock) {
        // Adds class so that the page can be styled to only show the "Checkout Method" step
        if ((this.currentStep == 'login' || this.currentStep == 'customstep') && section == 'customstep') {
            $j('body').addClass('opc-has-progressed-from-login');
        }

        if (reloadProgressBlock) {
            this.reloadProgressBlock(this.currentStep);
        }
        this.currentStep = section;
        var sectionElement = $('opc-' + section);
        sectionElement.addClassName('allow');
        this.accordion.openSection('opc-' + section);

        // Scroll viewport to top of checkout steps for smaller viewports
        if (Modernizr.mq('(max-width: ' + bp.xsmall + 'px)')) {
            $j('html,body').animate({scrollTop: $j('#checkoutSteps').offset().top}, 800);
        }

        if (!reloadProgressBlock) {
            this.resetPreviousSteps();
        }
    }
});

Be aware that the conditional statement has been changed so that a check is made if the section and this.currentStep is our customstep code.

this.currentStep is actually set as billing as originally defined in the opcheckout.js class. So copy over this property and change the value within the customcheckout.js file.

var Customcheckout = Class.create(Checkout, {
    initialize: function($super,accordion, urls){
        $super(accordion, urls);

        this.currentStep = 'customstep';

        this.steps = ['login', 'customstep' ,'billing', 'shipping', 'shipping_method', 'payment', 'review'];
    },
    ....
}

When you head to the checkout page, you should have a fully working checkout with your custom step added!

Magento Add Checkout Step

Note: This article is based on Magento Community/Open Source version 1.9.