Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

OneStep Checkout – A Magento Tutorial, Part 4 (Steps 11 and 12)

This is the last part of the long tutorial on the Magento checkout customization. here, I will describe the last checkout section “Order Review” and explain how the OneStep checkout link can be added to the Magento front-end.

Previous parts of this tutorial are: Part 1: Introducing OneStep Checkout, Part 2: Starting with the OneStep Checkout JavaScript, Part3: Deeper into the Checkout Sections.

Step 11: Order Review

This is the last section in the OneStep checkout. It contains an overview of the order contents: item list and order totals. In Magento OnePage checkout the Order Review is rendered in the last stage, when all the step data has already been submitted. The OneStep checkout must be showing an Order Review since the very beginning of the checkout process. But when a customer opens the OneStep checkout page, the current quote may not have enough data to generate an order summary. This is why the Order Review JavaScript attempts to fetch a section update HTML from the controller immediately after the OneStep checkout page is opened.

The JavaScript class representing the Order Review section is Review (classes/Review.js). To fetch the section update when the checkout page is displayed, the onestep.js script calls the updateReview method of the Review class (Listing 58).

review.updateReview(this, true);

Listing 58. Updating the Order Review section, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/onestep.js, line 36.

The OneStep checkout calls updateReview in two modes: with and without validation. The “with validation” mode is on when a customer is about to submit the order and the checkout must make sure that the data in every section form is complete and valid. The “without validation” mode skips checking the checkout data in the client to avoid displaying the validation error messages. The data is posted “as is” and the controller has to render a section update using whatever data it has. The “without validation” mode is necessary to allow the checkout JavaScript to update the Order Review section when the checkout page is opened for the first time and most of the section forms are empty. Trying to validate the update request at this stage will display validation error messages next to nearly every input element, which may scary customers off.

updateReview: function (event, noValidation) {
    var parameters = '', i, request, valid = false;

    noValidation = !!noValidation;

    valid = (checkout && checkout.validateReview(!noValidation));

    if (valid) {
        this.startLoader();

        for (i = 0; i < this.forms.length; i += 1) {
            if ($(this.forms[i])) {
                parameters += '&' + Form.serialize(this.forms[i]);
            }
        }

        parameters = parameters.substr(1);

        request = new Ajax.Request(
            this.getStepUpdateUrl,
            {
                method:     'post',
                onComplete: this.stopLoader.bind(this),
                onSuccess:  this.onUpdate,
                onFailure:  checkout.ajaxFailure.bind(checkout),
                parameters: parameters
            }
        );
    }
}

Listing 58. Method updateReview of the Review class, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Review.js, line 114.

In Listing 58, the updateReview method receives a noValidation parameter. If this parameter is true then the checkout instance will return true when calling its method validateReview. Next, the updateReview method serializes every checkout form into a parameters string which it then posts per Ajax to the OneStep controller method updateOrderReviewAction.

public function updateOrderReviewAction()
{
    if ($this->_expireAjax()) {
        return;
    }

    $post   = $this->getRequest()->getPost();
    $result = array('error' => 1, 'message' => Mage::helper('checkout')->__('Error saving checkout data'));

    if ($post) {

        $result            = array();
        $billing           = $post['billing'];
        $shipping          = $post['shipping'];
        $usingCase         = isset($billing['use_for_shipping']) ? (int) $billing['use_for_shipping'] : 0;
        $billingAddressId  = isset($post['billing_address_id']) ? (int) $post['billing_address_id'] : false;
        $shippingAddressId = isset($post['shipping_address_id']) ? (int) $post['shipping_address_id'] : false;
        $shippingMethod    = $this->getRequest()->getPost('shipping_method', '');
        $paymentMethod     = isset($post['payment']) ? $post['payment'] : array();

        /**
         * Attempt to save checkout data before loading the preview html
         * errors ignored
         */
        $this->saveAddressData($billing, $billingAddressId, 'billing', false);

        if ($usingCase <= 0) {
            $this->saveAddressData($shipping, $shippingAddressId, 'shipping', false);
        }

        if (!$this->getOnestep()->getQuote()->isVirtual()) {
            $this->saveShippingMethodData($shippingMethod, false);
        }

        $this->savePayment($paymentMethod, false);

        //update totals
        $this->getOnestep()->getQuote()->setTotalsCollectedFlag(false)->collectTotals()->save();

        $result['update_step']['review'] = $this->_getReviewHtml();
    }
    $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
}

Listing 59. Method updateOrderReviewAction of the OneStep controller, controllers/OnestepController.php, line 413.

In Listing 59, the updateOrderReviewAction method processes the posted checkout data. The controller and checkout model perform the server-side data validation but any errors that may have been produced must not displayed in the response. For this reason I pass false as the last parameter when calling methods saveAddressData, saveShippingMethodData, and savePayment. These methods have a response parameter that controls the writing of validation errors to the controller response.

protected function saveShippingMethodData($shippingMethod, $response = true)
{
    $result = $this->getOnestep()->saveShippingMethod($shippingMethod);

    if (isset($result['error'])) {
        if ($response) {
            $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
            return false;
        }
    }
    $this->getOnestep()->getQuote()->getShippingAddress()->setCollectShippingRates(true);

    return $result;
}

Listing 60. Method saveShippingMethodData and the response parameter, controllers/OnestepController.php, line 183.

The example in Listing 60 shows the saveShippingMethodData function that saves the selected shipping method. If the checkout model returns an error array, the saveShippingMethodData method will use the response parameter to decide whether to write the error message to the JSON output object or not. Passing false to the data saving methods in function updateOrderReviewAction allows the controller to produce an HTML update for the Order Review section without showing any validation error messages.

Method _getReviewHtml that generates the section HTML uses a layout update under handle checkout_onestep_review, which defines child blocks in the Order Review section template (Listing 61).

<checkout_onestep_review translate="label">
    <label>One Step Checkout Overview</label>
    <!-- Mage_Checkout -->
    <remove name="right"/>
    <remove name="left"/>

    <block
            type="checkout/onepage_review_info"
            name="checkout.review"
            output="toHtml"
            template="solvingmagento/onestepcheckout/review/info.phtml">
        <action method="addItemRender">
            <type>default</type>
            <block>checkout/cart_item_renderer</block>
            <template>checkout/onepage/review/item.phtml</template>
        </action>
        <action method="addItemRender">
            <type>grouped</type>
            <block>checkout/cart_item_renderer_grouped</block>
            <template>checkout/onepage/review/item.phtml</template>
        </action>
        <action method="addItemRender">
            <type>configurable</type>
            <block>checkout/cart_item_renderer_configurable</block>
            <template>checkout/onepage/review/item.phtml</template>
        </action>
        <block
                type="checkout/cart_totals"
                name="checkout.onestep.review.info.totals"
                as="totals"
                template="checkout/onepage/review/totals.phtml"/>
        <block
                type="core/text_list"
                name="checkout.onepage.review.info.items.before"
                as="items_before"
                translate="label">
            <label>Items Before</label>
        </block>
        <block
                type="core/text_list"
                name="checkout.onestep.review.info.items.after"
                as="items_after"
                translate="label">
            <label>Items After</label>
        </block>
    </block>
</checkout_onestep_review>

Listing 61. Layout configuration of the Order Review section, /app/design/frontend/base/default/layout/solvingmagento/onestepcheckout.xml, line 140.

The block classes and template files I use for the OneStep Order Review are the same ones used by Magento OnePage checkout with only one exception. The original OnePage checkout template for the checkout/onepage_review_info block contains the code for the “Order Submit” button. The specifics of the OneStep checkout required that I removed that code and implemented the submit button in its own block. I assigned the button block as a child to the Order Review container (Listing 62).

<div id="review-please-wait" class="step-please-wait" style="display: none">
    <span class="please-wait">
        <img
            src="<?php echo $this->getSkinUrl('images/opc-ajax-loader.gif') ?>"
            alt="<?php echo $this->__('Saving data, please wait...') ?>"
            title="<?php echo $this->__('Saving data, please wait...') ?>" class="v-middle" />
    </span>
</div>
<div class="checkout-review-update-container checkout-clear-both">
    <a href="#_"  id="checkout-review-update"><?php echo Mage::helper('checkout')->__('Update order review');?></a>
</div>
    <div class="order-review" id="checkout-load-review">
    <?php echo $this->getChildHtml('info') ?>
</div>

<div id="checkout-review-submit">
    <?php echo $this->getChildHtml('agreements') ?>
    <div class="buttons-set" id="review-buttons-container">
        <p class="f-left">
            <?php echo $this->__('Forgot an Item?') ?> <a href="<?php echo $this->getUrl('checkout/cart') ?>">
                <?php echo $this->__('Edit Your Cart') ?></a>
        </p>
        <?php echo $this->getChildHtml('button') ?>
    </div>
</div>

Listing 62. Order Review section container template, /app/design/frontend/base/default/template/solvingmagento/onestepcheckout/review.phtml, lines 1.

Listing 62 displays the Order Review section template whose child elements contain:

  • A “loading” GIF animation
  • A <div> with ID checkout-load-review where the JavaScript will insert the section update fetched from the controller
  • A line that will output a “checkout agreements” form
  • A line that will display the child button block containing the submit button.

The submit button of the OneStep checkout has two states: “Update order before placing” and “Place order”. The “Update order before placing” state is necessary because a customer can make changes to any checkout section any time and can never be sure that the Order Review display is accurate. To get a definite checkout state before placing the order, the customer must explicitly update the review section and validate the checkout data. If the validation is successful, the controller returns an updated Order Review section and the submit button changes its label to “Place Order”. This state change is reflected in the Review class property readyToSave which has value false unless the checkout data is valid.

Clicking the submit button invokes the submit method of the Review class (Listing 63).

submit: function () {

    var checkoutMethod, request, parameters = '', postUrl   = this.getStepUpdateUrl,
    onSuccess = this.onUpdate, i;

    /**
     * Submit order instead of updating only
     */
    if (this.readyToSave) {
        postUrl   = this.submitOrderUrl;
        onSuccess = this.onSuccess;
    }

    if (checkout && checkout.validateReview(true)) {
        this.startLoader();
        this.readyToSave = true;
        for (i = 0; i < this.forms.length; i += 1) {
            if ($(this.forms[i])) {
                parameters += '&' + Form.serialize(this.forms[i]);
            }
        }

        if (checkout.steps.login && checkout.steps.stepContainer) {
            checkoutMethod = 'register';
            $$('input[name="checkout_method"]').each(function (element) {
                if ($(element).checked) {
                    checkoutMethod = $(element).value;
                }
            });
            parameters += '&checkout_method=' + checkoutMethod;
        }
        parameters = parameters.substr(1);

        request = new Ajax.Request(
            postUrl,
            {
                method:     'post',
                onComplete: this.stopLoader.bind(this),
                onSuccess:  onSuccess,
                onFailure:  checkout.ajaxFailure.bind(checkout),
                parameters: parameters
            }
        );
    }
}

Listing 63. Method submit of the Review class, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Review.js, line 51.

In Listing 63, the submit method observes the readyToSave property to decide to which controller action the data must be posted. This property is true when the checkout data is validated and the Order Review section is updated. In this case, the submit method will post to the controller’s submitOrderAction method, otherwise the JavaScript will attempt to fetch an Order Review update by posting to the updateOrderReviewAction method. In both cases, the submit method calls checkout.validateReview with parameter true, which enables the client-side validation. If any of the checkout data is missing or incorrect, the JavaScript will display a validation error message next to the respective input element.

It may happen that a customer clicks the “Update order before placing” button, receives an Order Review update and then decides to change something in one of the checkout inputs. In this case, submitting an order must not be allowed until the checkout data is validated again. The Checkout class has a method observeChanges that registers a observer for the “change” event on every form input element in the OneStep checkout. The handler of this event sets the readyToSave property to false and changes the submit button’s label from “Place order” back to “Update order before placing” (Listing 64).

observeChanges: function () {
    'use strict';
    $$('div.osc-column-wrapper input').each(
        function (element) {
            Event.observe(
                $(element),
                'change',
                function () {
                    if (checkout.steps.review) {
                        checkout.steps.review.readyToSave = false;
                        if ($('order_submit_button')) {
                            $('order_submit_button').title = checkout.buttonUpdateText;
                            $('order_submit_button').down().down()
                                    .update(checkout.buttonUpdateText);
                        }
                    }
                }
            );
        }
    );
}

Listing 64. Method observeChanges in class Checkout, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Checkout.js, line 64.

The observeChanges method is also called from the Checkout class setResponse function (Listing 30, last line). The setResponse method uses the section updates delivered by the controller to update the DOM. The input elements in the update HTML have no “change” event observers, which is why the Checkout class has to call observeChanges after each such update.

When a customer clicks the “Place order” button, the JavaScript posts the serialized checkout form data to the OneStep controller method submitOrderAction. The composition of this method is similar to that of the updateOrderReviewAction in Listing 59 – the method receives the POST request data, parses it, and saves the section form data to the current quote using a checkout model instance. One thing different in the submitOrderAction method is checking if the customer has agreed to the required agreements (Listing 65).

if ($requiredAgreements = Mage::helper('checkout')->getRequiredAgreementIds()) {
    $postedAgreements = array_keys($this->getRequest()->getPost('agreement', array()));
    if ($diff = array_diff($requiredAgreements, $postedAgreements)) {
        $result['success'] = false;
        $result['error']   = true;
        $result['message'] = $this->__(
           'Please agree to all the terms and conditions before placing the order.'
        );
        $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
        return;
    }
}

Listing 65. Checking the agreements requirement, /app/code/local/Solvingmagento/OneStepCheckout/controllers/OnestepController.php, line 474.

The submitOrderAction method doesn’t ignore validation errors and adds error arrays returned by data saving methods to the $results variable. If this error list is not empty, the submitOrderAction method merges its entries into a single message and throws an exception. This exception is caught and the controller returns the error message to the checkout JavaScript which shows it in an alert; the order is not submitted (Listing 66).

foreach ($results as $stepResult) {
    if (isset($stepResult['error'])) {
        $result['error'] = 1;
        if (!isset($result['message'])) {
            $result['message'] = array();
        }
        if (isset($stepResult['message'])) {
            if (is_array($stepResult['message'])) {
                $result['message'] = array_merge($result['message'], $stepResult['message']);
            } else {
                $result['message'][] = $stepResult['message'];
            }
        }
    }
}
if (isset($result['error'])) {
    if ($result['message']) {
        throw new Mage_Core_Exception(implode("\n", $result['message']));
    }
}

Listing 66. Dealing with the server-side validation errors, /app/code/local/Solvingmagento/OneStepCheckout/controllers/OnestepController.php, line 519.

If saving the checkout data causes no errors, the controller creates a new order (Listing 67).

$this->getOnestep()->saveOrder();
$result['success'] = 1;

$redirectUrl = $this->getOnestep()->getCheckout()->getRedirectUrl();

Listing 67. submitting a new order, /app/code/local/Solvingmagento/OneStepCheckout/controllers/OnestepController.php, line 539.

It may happen that the selected payment method (e.g., PayPal Standard) requires a redirect to the payment provider website, where the customer must submit payment details. For this case, the controller checks if there is a redirectUrl property set to the checkout. If a redirect is required, the controller returns the URL to the checkout JavaScript that performs the redirect.

At the end, the OneStep checkout redirects to the “checkout success” page, which is the same used by the OnePage checkout. The checkout is complete.

Step 12: Making the OneStep checkout option visible in the front-end

This last step should’ve actually been the first because here I set up a link in the front-end, which customers can use to access the OneStep checkout. At the moment the “Proceed to Checkout” button takes the customer to the standard OnePage checkout. My extension doesn’t replace the OnePage checkout, so I will leave this button as is. The link to the OneStep checkout will be shown in the shop header next to the “My Cart” and “Login” links (Figure 11).

onestep_link

Figure 11. The “Checkout in One Step” link.

To output this link, I use a block of type Solvingmagento_OneStepCheckout_Block_Link. This block implements a single function, which checks if the OneStep checkout is enabled and, if yes, adds a link to this checkout to its parent block (Listing 68).

public function addOnestepCheckoutLink()
{
    if (!$this->helper('slvmto_onestepc')->oneStepCheckoutEnabled()) {
        return $this;
    }

    $parentBlock = $this->getParentBlock();
    if ($parentBlock && Mage::helper('core')->isModuleOutputEnabled('Solvingmagento_OneStepCheckout')) {
        $text = $this->__('Checkout in One Step');
        $parentBlock->addLink(
            $text,
            'checkout/onestep',
            $text,
            true,
            array('_secure' => true),
            60,
            null,
            'class="top-link-checkout"'
        );
    }
    return $this;
}

Listing 68. Generating the “Checkout in One Step” link, Block/Link.php, line 43.

The OneStep checkout layout updates file adds the Link block as a child to the Magento’s “top.links” block (Listing 69).

<default>
    <reference name="top.links">
        <block type="slvmto_onestepc/link" name="onestep_checkout_link">
            <action method="addOnestepCheckoutLink"></action>
        </block>
    </reference>
</default>

Listing 69. Adding the link to the top.link block, /app/design/frontend/base/default/layout/solvingmagento/onestepcheckout.xml, line 19.


The <action> node triggers the addOnestepCheckoutLink method of the Link block, which adds the link to the OneStep checkout to the front-end.

Conclusion

Building a OneStep checkout has been a long and difficult journey. While the finished extension is a working solution, it is a product of many compromises which were necessary to make this OneStep checkout work for the same scenarios as the standard OnePage checkout. As I’ve explained at the beginning of the tutorial, OneStep works best in situations where checkout forms are short and simple. The form I’ve had to deal with when building this extension is nothing like that. If you decide to use this extension in a real project, be prepared to reconsider some of the compromises I made and adapt the code and layout to the specific needs of your shop.

The finished extension is available at GitHub at Solvingmagento_OneStepCheckout. You can try it in the test shop I’ve set up at osc.solvingmagento.net. There, you can use a registered customer account john.doe@example.com with password password.

Readers who read this post also read these:

14 thoughts on “OneStep Checkout – A Magento Tutorial, Part 4 (Steps 11 and 12)

  1. Checkout is the most completed part in magento. Your onestep checkout tutorials and onepage checkout tutorials give me enough power to modify checkout flow to fit my client’s need. Thank you very much!

  2. Thanks for this great Tutorial :-)

    I have implemented one step checkout after reading this tutorial. Thanks

    But couldn’t make it work with Ebizmarts Sagepay module. :-(

  3. Hi,
    It is great work but in my case when I click on log in then it shows error

    Not Found

    The requested URL /customer/account/login was not found on this server.

    Apache/2.4.9 (Win64) PHP/5.5.12 Server at magentoshop.com Port 80

  4. Thanks for this great Tutorial :-)
    i have find it doesn’t work in ie9, i change the css that it can’t show in ie ,but the jquery doesn’t work well, can you help me ?thx.

Leave a Reply

Your email address will not be published. Required fields are marked *

Theme: Esquire by Matthew Buchanan.