Adding Checkout Steps in Magento 1

Below you'll find the code I used to add a checkout step to Magento's RWD theme. Despite there being many checkout extensions that reduce the number of checkout steps from six to one, website owners do not always like opting to install third party code.

Part of the reason is because the code is written by third party developers who might not conform to your own standards. Secondly, not all payment method integrations with Magento are compatible with checkout extensions. Understandably, many clients like to use the default six step checkout provided.

You might want an additional checkout step to display some important information to the customer, or get the customer to fill out some information before proceding to the next step.

Use the code below to add a step just after the login step of the checkout. This has been tested with Magento Community/Open Source version 1.9.3.6.

1. Add the custom module's declaration file.

file: app/etc/modules/SussexDev_CheckoutStep.xml
<?xml version="1.0"?>
<config>
    <modules>
        <SussexDev_CheckoutStep>
            <codePool>local</codePool>
            <active>true</active>
        </SussexDev_CheckoutStep>
    </modules>
</config>

2. Define the module's config.xml file and add the block prefix configuration.

file: app/code/local/SussexDev/CheckoutStep/etc/config.xml
<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <sussexdev_checkoutstep>
                <class>SussexDev_CheckoutStep_Block</class>
            </sussexdev_checkoutstep>
        </blocks>
    </global>
</config>

3. Add the block defining the custom step. Each checkout step should have its own unique identifier. In this example, I use customstep.

file: app/code/local/SussexDev/CheckoutStep/Block/CHeckout/Onepage/Customstep.php
<?php
class SussexDev_CheckoutStep_Checkout_Onepage_Customstep extends Mage_Checkout_Block_Onepage_Abstract
{
    protected function _construct()
    {
        $this->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();
    }
}

4. Add in the checkout block rewrite configuration to config.xml to override the getSteps() method to add in the custom step code.

file: app/code/local/SussexDev/CheckoutStep/etc/config.xml
<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <sussexdev_checkoutstep>
                <class>SussexDev_CheckoutStep_Block</class>
            </sussexdev_checkoutstep>
            <checkout>
                <rewrite>
                    <onepage>SussexDev_CheckoutStep_Block_Checkout_Onepage</onepage>
                </rewrite>
            </checkout>
        </blocks>
    </global>
</config>

5. Proceed with overriding getSteps().

file: app/code/local/SussexDev/CustomStep/Block/Checkout/Onepage.php
<?php
class SussexDev_CustomStep_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';
    }

}

6. Define a layout file to be used within config.xml.

file: app/code/local/SussexDev/CheckoutStep/etc/config.xml
<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <sussexdev_checkoutstep>
                <class>SussexDev_CheckoutStep_Block</class>
            </sussexdev_checkoutstep>
            <checkout>
                <rewrite>
                    <onepage>SussexDev_CheckoutStep_Block_Checkout_Onepage</onepage>
                </rewrite>
            </checkout>
        </blocks>
    </global>
    <frontend>
        <layout>
            <updates>
                <sussexdev_customstep>
                    <file>sussexdev/checkoutstep.xml</file>
                </sussexdev_customstep>
            </updates>
        </layout>
    </frontend>
</config>

7. Within the layout file, override the template of the onepage.phtml file and also include the template for our additional checkout step.

Note that the block name used when defining the block to be used for the additional checkout step must match the step code identifier being used. As seen below, the block name is set to customstep.

file: app/design/frontend/base/default/layout/sussexdev/customstep.xml
<?xml version="1.0"?>
<layout>
    <checkout_onepage_index>
        <reference name="checkout.onepage">
            <action method="setTemplate">
                <template>sussexdev_checkoutstep/onepage.phtml</template>
            </action>
            <block type="sussexdev_checkoutstep/checkout_onepage_customstep" name="customstep"
                   template="sussexdev_checkoutstep/onepage/customstep.phtml" />
        </reference>
    </checkout_onepage_index>
</layout>

8. Within the new onepage.phtml template file, copy over the contents from the RWD theme's onepage.phtml but include an additional JavaScript file.

file: app/design/frontend/base/default/template/sussexdev_checkoutstep/onepage.phtml
<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('sussexdev_checkoutstep/js/customstep.js') ?>"></script>

<ol class="opc opc-firststep-<?php echo $this->getActiveStep() ?>" id="checkoutSteps">
    <?php $i=0; foreach($this->getSteps() as $_stepId => $_stepInfo): ?>
        <?php if (!$this->getChild($_stepId) || !$this->getChild($_stepId)->isShow()): continue; endif; $i++ ?>
        <li id="opc-<?php echo $_stepId ?>" class="section<?php echo !empty($_stepInfo['allow'])?' allow':'' ?><?php echo !empty($_stepInfo['complete'])?' saved':'' ?>">
            <div class="step-title">
                <span class="number"><?php echo $i ?></span>
                <h2><?php echo $_stepInfo['label'] ?></h2>
                <a href="#"><?php echo $this->__('Edit') ?></a>
            </div>
            <div id="checkout-step-<?php echo $_stepId ?>" class="step a-item" style="display:none;">
                <?php echo $this->getChildHtml($_stepId) ?>
            </div>
        </li>
    <?php endforeach ?>
</ol>
<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 CheckoutStep(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>

9. Within the JavaScript file defined, add in the custom step code to the steps array.

file: skin/frontend/base/default/sussexdev_checkoutstep/js/checkoutstep.js
var CheckoutStep = 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;
        }
    }
});

10. Heading back to onepage.phtml, change the default Checkout class to use the new CheckoutStep class defined.

file: app/design/frontend/base/default/template/sussexdev_checkoutstep/onepage.phtml
<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 CheckoutStep(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>

11. Create the customstep.phtml file as defined in the layout configuration. In this example, I'll simply create some text with a continue button to proceed to the next checkout step.

file: app/design/frontend/base/default/template/sussexdev_checkoutstep/onepage/customstep.phtml
<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>

12. Define a CustomStep JavaScript class is used in the template. This class can go in the JavaScript file used previously, underneath the CheckoutStep class code.

file: skin/frontend/base/default/sussexdev_checkoutstep/js/checkoutstep.js
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();
    }
}

13. The save checkout step URL that was specified in the customstep.phtml file needs to be added into the module. Start by defining the router configuration in config.xml.

file: app/code/local/SussexDev/CheckoutStep/etc/config.xml
<?xml version="1.0"?>
<config>
    <global>
        <blocks>
            <sussexdev_checkoutstep>
                <class>SussexDev_CheckoutStep_Block</class>
            </sussexdev_checkoutstep>
            <checkout>
                <rewrite>
                    <onepage>SussexDev_CheckoutStep_Block_Checkout_Onepage</onepage>
                </rewrite>
            </checkout>
        </blocks>
    </global>
    <frontend>
        <layout>
            <updates>
                <sussexdev_customstep>
                    <file>sussexdev/checkoutstep.xml</file>
                </sussexdev_customstep>
            </updates>
        </layout>
        <routers>
            <checkout>
                <args>
                    <modules>
                        <sussexdev_checkoutstep before="Mage_Checkout">SussexDev_CheckoutStep</sussexdev_checkoutstep>
                    </modules>
                </args>
            </checkout>
        </routers>
    </frontend>
</config>

14. Now add the controller. Usually the controller would be responsible for saving 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.

file: app/code/local/SussexDev/CheckoutStep/controllers/OnepageController.php
<?php
require_once 'Mage/Checkout/controllers/OnepageController.php';
class SussexDev_CheckoutStep_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));
    }
}

15. Override the gotoSection function from the Checkout class so that the opc-has-progressed-from-login class is added when we arrive on the customstep checkout step rather than the billing step.

file: skin/frontend/base/default/sussexdev_checkoutstep/js/checkoutstep.js
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();
        }
    }
});

16. Lastly, within initialize in the CheckoutStep class, set the this.currentStep property to customstep.

file: skin/frontend/base/default/sussexdev_checkoutstep/js/checkoutstep.js
var CheckoutStep = Class.create(Checkout, {
    initialize: function($super,accordion, urls){
        $super(accordion, urls);

        this.currentStep = 'customstep';

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

You should now have a perfectly working checkout step available on your checkout! Magento doesn't make it easy adding a checkout step, and there is more complicated functionality you could want on your checkout than the example I gave above.

If you need any help with integrating functionality on your checkout step, always remember that your friendly Magento Developer in Sussex is around to help.