Sunday, November 13, 2011

Adding a step to the Onepage Checkout

The default Magento onepage checkout includes six steps for the customer to complete. However, sometimes you may have a requirement to create an extra checkout step. An example of this might be an option for your customer to choose a free gift as part of their order, or an extra step to collect special delivery instructions. Using delivery instructions as an example, we'll demonstrate how this can be achieved.

The first file we need to modify is app/code/core/Mage/Checkout/Block/Onepage.php. Obviously we don't want to modify the code in the core context, so copy the file to app/code/local/Mage/Checkout/Block/Onepage.php. Magento will use this file automatically.

In the getSteps() method of this class there is an array of the step codes, in order of viewing. We need to add our own step code in this array, in the relevant place. For this example, the code will be deliveryinstructions so we will change the line to be this:

$stepCodes = array('billing', 'shipping', 'shipping_method', 'deliveryinstructions', 'payment', 'review');

Next, we need to create a new file - app/code/local/Mage/Checkout/Block/Onepage/Deliveryinstructions.php. As you can see from the name, this is the block file that runs our new step. You can use this class to do any special setup work for your block, but more than likely all you'll need is this:

<?php
class Mage_Checkout_Block_Onepage_Deliveryinstructions extends Mage_Checkout_Block_Onepage_Abstract
{
    protected function _construct()
    {
        $this->getCheckout()->setStepData('deliveryinstructions', array(
            'label'     => Mage::helper('checkout')->__('Delivery Instructions'),
            'is_show'   => $this->isShow()
        ));
        parent::_construct();
    }
}

Now we need to make the actual template. The file we need is: <THEME DIRECTORY>/template/checkout/onepage/deliveryinstructions.phtml. Often the best way to create this file is to copy one of the other files, perhaps billing.phtml or shipping.phtml. Then, just modify the file to suit your needs.

With the template created, we need to tell Magento about it by editing the <THEME DIRECTORY>/layout/checkout.xml file. This will tell Magento when to load our new template file. Below the multishipping sections in this file the <checkout_onepage_index> context begins. Look through it until you find the references to the login, billing, shipping, shipping method, payment and review templates. These are all inside the content reference area. At the position between all these files that you want your step to show, you need to add a reference to your new template. It should look something like this:

<block type="checkout/onepage_deliveryinstructions" name="checkout.onepage.deliveryinstructions" as="deliveryinstructions" template="checkout/onepage/deliveryinstructions.phtml"/>

Following on from that, we need to override the onepage controller to put in our extra step. How to actually override a controller is beyond the scope of this post, however the Magento wiki article on overriding controllers should tell you everything you need to know.

There are two parts we need to work with in our overridden controller, and the first one is the save<STEP>Action method for the step before ours. In this example, I'm putting my new step between the Shipping Method step and the Payment Method step, so I'm going to override the saveShippingMethodAction method. We need to change the goto_section in the method to be our new step, so it should look like this:

$result['goto_section'] = 'deliveryinstructions';

We also need to remove the following code, as we don't need to update our delivery instructions section. We will need the code again for the next part, so keep it handy:

$result['update_section'] = array(
        'name' => 'payment-method',
        'html' => $this->_getPaymentMethodsHtml()
);

The next change to this controller should be to add a new method, called saveDeliveryinstructionsAction. This method must perform two functions; it should call the method on the Onepage Model, and also pass any data back to the browser. The following code is an example of what we need, copied and modified from the saveShippingMethodAction method:

public function saveDeliveryinstructionsAction()
{
 $this->_expireAjax();
 if ($this->getRequest()->isPost()) {
  $data = $this->getRequest()->getPost('deliveryinstructions', '');
  $result = $this->getOnepage()->saveDeliveryinstructions($data);
  /*
  $result will have error data if shipping method is empty
  */
  if(!$result) {
   Mage::dispatchEvent('checkout_controller_onepage_save_deliveryinstructions', array('request'=>$this->getRequest(), 'quote'=>$this->getOnepage()->getQuote()));
   $this->getResponse()->setBody(Zend_Json::encode($result));

   $result['goto_section'] = 'payment';
   $result['update_section'] = array(
       'name' => 'payment-method',
       'html' => $this->_getPaymentMethodsHtml()
   );

  }
  $this->getResponse()->setBody(Zend_Json::encode($result));
 }
}

In the last step we called a method that we haven't defined yet, so we'll do that now. This method is in the app/code/core/Mage/Checkout/Model/Type/Onepage.php file. Like the past few files, we are going to modify the saveShippingMethod method and also copy it to a new method. The saveShippingMethod code should be modified to look like this:

public function saveShippingMethod($shippingMethod)
{
    if (empty($shippingMethod)) {
        $res = array(
            'error' => -1,
            'message' => Mage::helper('checkout')->__('Invalid shipping method.')
        );
        return $res;
    }
    $rate = $this->getQuote()->getShippingAddress()->getShippingRateByCode($shippingMethod);
    if (!$rate) {
        $res = array(
            'error' => -1,
            'message' => Mage::helper('checkout')->__('Invalid shipping method.')
        );
        return $res;
    }
    $this->getQuote()->getShippingAddress()->setShippingMethod($shippingMethod);
    $this->getQuote()->collectTotals()->save();

    $this->getCheckout()
        ->setStepData('shipping_method', 'complete', true)
        ->setStepData('deliveryinstructions', 'allow', true);

    return array();
}

Unfortunately, there is no simple way to save the data that we collect. For example, information about a customer will be saved completely differently to shipping information like our delivery instructions; the delivery instructions will be saved differently again to how we would add a free gift to the order.

Because of this I won't show you any code for actually saving the data you've captured, though we may address this in a follow up post at a later date. The following code is the new method that does not save anything:

public function saveDeliveryinstructions($deliveryinstructions)
{
    // Save the data here

    $this->getCheckout()
        ->setStepData('deliveryinstructions', 'complete', true)
        ->setStepData('payment', 'allow', true);

    return array();
}

Next, we need to make a couple of JavaScript changes. These changes will be made in the <SKIN DIRECTORY>/js/opcheckout.js> file. In the Checkout class, we need to change the setShippingMethod method to go to the delivery information block, similar to the way we have in the files above. Then we need to make a setDeliveryinstructions method which will be exactly the same as the setShippingMethod method before we modified it.

There is a class further down in the file called ShippingMethod, inside of which is a method called nextStep. We need to change it as per the above method so that the step goes to the delivery instructions step rather than the payment step. Then, again like before, we need to copy the whole class and modify it so that it's now a DeliveryInstructions class, making sure to set the next step to be the payment step.

There is now one final requirement, which is to add in our step in the progress meter on the right side of the checkout. To do this we need to edit <THEME DIRECTORY>/template/checkout/onepage/progress.phtml. If you look at the file, you'll have a pretty good idea of how the template should be laid out and below is an example of what to add in for our example delivery instructions:

<?php if ($this->getCheckout()->getStepData('deliveryinstructions', 'is_show')): ?>
 <?php if ($this->getCheckout()->getStepData('deliveryinstructions', 'complete')): ?>
  <li>
   <h4 class="complete"><?php echo $this->__('Delivery Instructions') ?> <span class="separator">|</span> <a href="#deliveryinstructions" onclick="checkout.accordion.openSection('opc-deliveryinstructions');return false;"><?php echo $this->__('Change') ?></a></h4>
   <div class="content">
    <!-- Here write code to display the data the customer enters -->
   </div>
  </li>
 <?php else: ?>
  <li>
   <h4><?php echo $this->__('Delivery Instructions') ?></h4>
  </li>
 <?php endif; ?>
<?php endif; ?>

We have now added a new step to our checkout and made it accessible in the same way as the rest of the checkout steps. Using this as a base, you can develop the concept further to provide additional information or options to the customer during checkout. Of course, too many steps will likely turn people away, so it's a matter of balancing the need to collect extra information with the need to make the checkout as quick and easy to complete as possible.

No comments:

Post a Comment