Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

OneStep Checkout – A Magento Tutorial, Part 3 (Steps 8 – 10 of 12)

In the third part of the Magento OneStep checkout tutorial I will continue describing the JavaScript and controller functionality of the OneStep checkout sections. See also the parts one and two.

If you are impatient, you can download the finished extension from GitHub Solvingmagento_OneStepCheckout now. A demo shop with a working OneStep checkout can be found here.

Other parts of this tutorial are: Part 1: Introducing OneStep Checkout, Part 2: Starting with the OneStep Checkout JavaScript, Part4: Order Review.

Step 8: Billing and Shipping Information

In this step, I will describe files and classes that process the billing and shipping address information:

The functionality required in the Billing and Shipping Information sections is this:

  1. Authenticated customers must be able to select billing and shipping addresses from their account’s address book entries or to enter a new address.
  2. Non-authenticated customers can only see the new address form.
  3. Customers must be able to use the billing address as a shipping address. This choice must be reflected in the Shipping Information interface: the address form in this section must be automatically populated with the data from the Billing Information form.

To implement the first requirement, I must make sure that the template files of the address sections output a list of addresses from the customer address book (Listing 33).

<?php if ($this->customerHasAddresses()): ?>
    <li class="fields" id="billing-address-select">
        <label>
            <?php echo $this->__('Select a billing address from your address book or enter a new address.') ?>
        </label>
        <div class="input-box">
            <?php echo $this->getAddressesHtmlSelect('billing') ?>
        </div>
    </li>
<?php endif; ?>

Listing 33. Displaying a list of available customer addresses in the billing address form, /app/design/frontend/base/default/template/solvingmagento/onestepcheckout/billing.phtml, line 22.

Listing 33 demonstrates a conditional output of the address book entries for the Billing Information section template (the respective code in the Shipping Information template is nearly identical). By calling the $this->customerHasAddresses() function, Magento fetches a customer instance and checks if the customer account has addresses. The customerHasAddresses method is inherited from the Mage_Checkout_Block_Onepage_Abstract class, which is extended by the Solvingmagento_OneStepCheckout_Block_Onestep_Billing block. The output of the address list is generated by the Billing Information block function getAddressesHtmlSelect (Listings 34 and 35).

$options = array();
foreach ($this->getCustomer()->getAddresses() as $address) {
    $options[] = array(
    'value' => $address->getId(),
    'label' => $address->format('html')
    );
}

Listing 34. Fetching a list of customer addresses in method getAddressesHtmlSelect, Block/Onestep/Billing.php, line 33.

In Listing 34, Magento iterates through the customer addresses and generates an array consisting of the address IDs and string representations of the address. The latter is produced by calling the format function on the address object. This call uses the “html” address output setting, which you can find in the shop back-end under System > Configuration > Customers > Customer Configuration > Address Templates.

Next in method getAddressesHtmlSelect, Magento has to figure out which address is currently selected. If the quote has a saved address, then its ID is set to the $addressId variable, otherwise the ID of either the default billing or the default shipping address is used. The $addressId variable is later used to pre-select the respective entry in the address list (Listing 35).

$addressId = $this->getAddress()->getCustomerAddressId();
if (empty($addressId)) {
    if ($type=='billing') {
        $address = $this->getCustomer()->getPrimaryBillingAddress();
    } else {
        $address = $this->getCustomer()->getPrimaryShippingAddress();
    }
    if ($address) {
        $addressId = $address->getId();
    }
}

$html = '';
foreach ($options as $option) {

    $html .= '<div><input type="radio" name="' . $type . '_address_id" value="' . $option['value'] . '"' .
        ' id="' . $type . '-address-id-' . $option['value'] .'"';

    if ($option['value'] == $addressId) {
        $html .= ' checked="checked"';
    }

    $html .= '/><label for="' . $type . '-address-id-' . $option['value'] . '">' . $option['label'] . '</label>'
        . PHP_EOL;

    $html .= '<div class="validation-advice advice-required-entry-billing-address-id"'
        . ' style="display:none">' . Mage::helper('checkout')->__('This is a required field.') . '</div>'
        . '<div style="height: 1px; clear:both"></div></div>' . PHP_EOL;
}

$html .= '<div><input type="radio" name="' . $type . '_address_id" value="" id="' . $type . '-address-id-">';
$html .= '<label for="' . $type . '-address-id-">' . Mage::helper('checkout')->__('New address') . '</label>';
$html .= '<div class="validation-advice advice-required-entry-billing-address-id" style="display:none">'
    . Mage::helper('checkout')->__('This is a required field.') . '</div>';
$html .= PHP_EOL . '<div style="height: 1px; clear:both"></div></div>' . PHP_EOL;

return $html;

Listing 35. Rendering a customer address list in method getAddressesHtmlSelect, Block/Onestep/Billing.php, line 41.

In Listing 35, Magento iterates through the available addresses and produces a list of radio buttons whose labels are HTML-formatted address lines. To each of these radio buttons Magento adds a <div> element containing a validation advice entry. Finally, Magento adds one more option – “New address” – which when selected will display the new address form.

You can see an example of the getAddressesHtmlSelect method’s output in Figure 9.

address_list

Figure 9. Customer address lists in the OneStep checkout.

The “New address” form allows both authenticated and non-authenticated customers to enter a new billing or shipping address. When a logged-in customer opens the checkout, the “New address” form is hidden. To show it, the customer must select the “New address” option as shown in Figure 10.

The template code of the “New address” form is long and, apart of a few minor alterations, is the same as the one used by the OnePage checkout. For these reasons I don’t show it here, but you can download the template files (billing.phtml and shipping.phtml) from GitHub.

new_address

Figure 10. The “New address” form.

In Figure 10, you can also see radio buttons “Ship to this address” and “Ship to different address” under the Billing Information section and a checkbox “Use Billing Address” under the Shipping Information section. These control elements allow customers to use their selected billing address as the shipping destination. The click events of these buttons are observed by the JavaScript code in classes Billing and Shipping (Listing 36).

$$('input[name="billing[use_for_shipping]"]').each(
    function (element) {
        if (!!element.checked) {
            $('shipping:same_as_billing').checked = !!element.value;
        }
    }
);

$$('input[name="billing[use_for_shipping]"]').each(
    function (element) {
        Event.observe(
            $(element),
            'change',
            this.toggleSameAsBilling.bindAsEventListener(this)
        );
    }.bind(this)
);

Event.observe(
    $('shipping:same_as_billing'),
    'change',
    function (event) {
        if (Event.element(event).checked) {
            this.setSameAsBilling(true);
            $('billing:use_for_shipping_yes').checked = true;
            $('billing:use_for_shipping_no').checked  = false;
        }
    }.bind(this)
);

Listing 36. Observing the customer choice to use the billing address as shipping, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Shipping.js, line 46.

Listing 36 displays the contents of the initialize method of the Shipping JavaScript class representing the Shipping Information section. In this listing the first block is executed when the shipping section is about to be displayed. This block checks whether the “Ship to this address” is checked in the Billing Information form and toggles the checked status of the “Use Billing Address” checkbox accordingly.

The second code block in Listing 36 registers an observer to the”change” event of the “use_for_shipping” radio buttons on the Billing Information section. The event handler is the Shipping class method toggleSameAsBilling shown in Listing 37.

toggleSameAsBilling: function () {
    var value = false;

    $$('input[name="billing[use_for_shipping]"]').each(
        function (element) {
            if (!!element.checked) {
                value = !!parseInt(element.value, 10);
            }
        }
    );

    //value === true : same as billing
    //value === false : different shipping address

    if (value) {
        this.setSameAsBilling(true);
    } else {
        this.resetAddress();
    }
}

Listing 37. Method toggleSameAsBilling, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Shipping.js, line 133.

The toggleSameAsBilling method in Listing 37 checks the customer choice of the use_for_shipping option in the Billing Information step, and, if the “Ship to this address” is selected, invokes the setSameAsBilling method; otherwise, the shipping address form data is reset.

The last code block in Listing 36 registers an observer to the “change” event of the “Use Billing Address” checkbox. When this box is checked, the event handler invokes the setSameAsBilling method of the Shipping class. The setSameAsbilling method copies the fields of the Billing Information form into their shipping address counterparts (Listing 38).

setSameAsBilling: function (flag) {
    var arrElements,
        elemIndex,
        billingId;

    if (flag) {
        arrElements = Form.getElements($('co-shipping-form'));
        for (elemIndex in arrElements) {
            if (arrElements.hasOwnProperty(elemIndex)) {
                if (arrElements[elemIndex].id) {
                    billingId = arrElements[elemIndex].id.replace(/^shipping/, 'billing');
                    if ((billingId === 'billing:region_id') && shippingRegionUpdater) {
                        shippingRegionUpdater.update();
                    }
                    arrElements[elemIndex].value = ($(billingId) && $(billingId).value) ? $(billingId).value : '';
                    if ($(billingId) && !!$(billingId).checked) {
                        arrElements[elemIndex].checked = true;
                    }
                    if ($(billingId)
                            && ($(billingId).name === 'billing_address_id')
                            && (!$(billingId).value)
                            ) {
                        this.newShippingAddress();
                    }
                }
            }
        }
    } else {
        $('shipping:same_as_billing').checked = false;
    }
},

Listing 38. Method setSameAsBilling, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Shipping.js, line 150.

Note that in Listing 38, if the “New Address” option is selected in the Billing Information section, Magento will invoke the newShippingAddress method of the Shipping class which will display the “New address” form of the shipping section.

Step 9: Shipping Method

In this section, the customer must select a shipping option. Listing 16 shows the block configuration for the Shipping Method section, which consists of a parent block of type slvmto_onestepc/onestep_shipping_method and its child checkout/onepage_shipping_method_available. The former renders a container template which displays the section title, a “loading” GIF image, and the opening and closing tags of the section form. This template also contains a <div> element with ID checkout-load-shipping_method used by the JavaScript to load an updated list of the shipping methods when the checkout state changes (Listing 39).

<div id="shipping_method-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-clear-both"></div>
<div class="onestep-step">
    <form id="co-shipping-method-form" action="">
        <div id="checkout-load-shipping_method">
            <?php echo $this->getChildHtml('available') ?>
        </div>
        <div id="reload-shipping-method">
            <a href="#" id="reload-shipping-method-button"><?php echo $this->__('Update shipping options');?></a>
        </div>
    </form>
</div>
<div id="shipping_method-advice-source" style="display: none">
    <div class="validation-advice advice-required-entry-shipping_method" style="display:none">
        <?php echo Mage::helper('checkout')->__('This is a required field.');?>
    </div>
</div>

Listing 39. The container template of the Shipping Information section, /app/design/frontend/base/default/template/solvingmagento/onestepcheckout/shipping_method.phtml, line 18.

Updating the Shipping Method Section

When the checkout page is opened for the first time, the quote doesn’t have an address that it needs to produce a shipping method list. In this stage, the Shipping Method section contains only a link “Update shipping options” (Figure 7). When a customer clicks this link the checkout, the JavaScript attempts to save the form data of the Billing and Shipping Information sections to the quote. If this request is successful, the controller will return a shipping method list which the JavaScript then shows to the customer.

This JavaScript functionality is implemented in class ShippingMethod. In its initialize method, I register an observer of the “click” event of the link element with ID reload-shipping-method-button (Listing 40).

Event.observe(
    $('reload-shipping-method-button'),
    'click',
    this.getStepUpdate.bindAsEventListener(this)
);

Listing 40. Observing the click event of the “Update shipping options” link, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/ShippingMethod.js, line 41.

The “click” event handler in Listing 40 is the getStepUpdate method that posts the address data to the controller (Listing 41).

getStepUpdate: function () {
    var request, parameters = {}, valid = false;

    if ($('shipping:same_as_billing').checked && checkout.steps.shipping) {
        checkout.steps.shipping.setSameAsBilling(true);
    }

    if (checkout) {
        valid = checkout.validateCheckoutSteps(['CheckoutMethod', 'BillingAddress', 'ShippingAddress']);
    }

    if (valid) {
        this.startLoader();

        parameters =  Form.serialize('co-billing-form') + '&' + Form.serialize('co-shipping-form');

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

Listing 41. Method getStepUpdate of the ShippingMethod class, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/ShippingMethod.js, line 110.

In Listing 41, the getStepUpdate method first checks if the customer has chosen to use the billing address as shipping and, if yes, populates the shipping address form with the billing address data. Next, the getStepUpdate method uses the checkout object to validate the section form data which it is about to post. If the validation is successful, the method posts the serialized section form data to the OneStep controller updateShippingMethodsAction method (the property this.getStepUpdateUrl gets its value /checkout/onestep/updateShippingMethods in the initialize method).

A necessary detour: validating the checkout form data

At this point I have to explain how the OneStep JavaScript performs the client-side validation of the form data. Every time I need to post data to the controller, I must make sure that all the required fields are filled out with correct data. For this purpose, I use a checkout instance of the Checkout class which is available to every checkout section class. The validation method is validateCheckoutSteps and its contents is shown in Listing 42.

validateCheckoutSteps: function (steps) {
    var step, result = true;
    for (step in steps) {
        if (steps.hasOwnProperty(step)) {
            if (this['validate' + steps[step]]) {
                result = this['validate' + steps[step]]() && result;
            }
        }
    }
    return result;
}

Listing 42. The validateCheckoutSteps method of the Checkout class, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Checkout.js, line 331.

In Listing 42, the validateCheckoutSteps method receives an array containing names of the sections (steps) which must be validated. You can see an example of such call in Listing 41. For every section JavaScript calls a Checkout class method whose name begins with “validate” and ends with the section name. These methods are:

  • validateCheckoutMethod
  • validateBillingAddress
  • validateShippingAddress
  • validateShippingMethod
  • validatePaymentMethod
  • validateReview

Generally, the validation routine checks if the section form has a list of radio buttons of which one must be selected. If no option is checked, the validation method shows an error message (contained within a hidden <div> with an ID like “advice-required-entry-{stepId}”). If a section also contains other form elements, like the input fields of an address form, or a child form of a payment method, I also use the standard Magento form validation by instantiating a Prototype.js Validation object on the form container. As an example, lets consider the method validateAddress in Listing 43.

validateAddress: function (type) {

    if (type  !== 'billing' && type !== 'shipping') {
        return false;
    }
    if (!window[type]) {
        return false;
    }

    if (!window[type].stepContainer) {
        //the step is not shown, no validation required
        return true;
    }
    var validationResult         = false,
        newAddressFormValidation = false,
        validator                = new Validation('co-' + type + '-form');

    newAddressFormValidation = validator.validate();

    $$('div.advice-required-entry-' + type + '-address-id').each(
        function (element) {
            $(element).hide();
        }
    );
    if ($$('input[name="' + type + '_address_id"]')
            && $$('input[name="' + type + '_address_id"]').length > 0
            ) {
        $$('input[name="' + type + '_address_id"]').each(
            function (element) {
                if ($(element).checked) {
                    validationResult = true;
                }
            }
        );
        if (!validationResult) {
            $$('div.advice-required-entry-' + type + '-address-id').each(
                function (element) {
                    $(element).show();
                }
            );
        }
    } else {
        validationResult = true;
    }
    return (newAddressFormValidation && validationResult);
}

Listing 43. Validating address form data in method validateAddress. /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Checkout.js,, line 114.

The validateAddress method is called by the validateBillingAddress and validateShippingAddress functions that pass their respective address type: billing or shipping. In Listing 43, I first check if an instance of the section class exists – window[type] points to the global variable window['billing'] or window['shipping'], which elsewhere are referred to as simply billing and shipping. Next, I check if these instances have a stepContainer property. For example, shipping, when hidden in a case of a virtual quote checkout, may not have an initialized stepContainer which makes the further validation unnecessary (the method will return true).

Address sections consist of a radio button list representing the available address book entries and of a “New address” form. To validate the latter, I use a Validation class instance validator (Listing 44).

newAddressFormValidation = validator.validate();

Listing 44. Validating the “New address” form in method validateAddress. /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Checkout.js, line 132.

Next, I check if the radio button list exists (in a case of a logged-in customer who has entries in his address book). The radio buttons are accessed by referring to the $$('input[name="' + type + '_address_id"]') list. If this list is not empty, I iterate through it checking if any option is selected.

Finally, the method returns the address validation result as true, if:

  • the customer has selected one of its existing addresses, or
  • has filled out the “New address” form (the newAddressFormValidation variable equals true)

Otherwise, the validation fails and the JavaScript automatically displays the corresponding error messages.

In a similar fashion, the Checkout class implements other validation methods.

Back to the Shipping Method Section

The data posted by the getStepUpdate method are dispatched to the OneStep controller method updateShippingMethodsAction (Listing 45).

public function updateShippingMethodsAction()
{
    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;

        if ($this->saveAddressData($billing, $billingAddressId, 'billing') === false) {
            return;
        }

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

        /* check quote for virtual */
        if ($this->getOnestep()->getQuote()->isVirtual()) {
            $result['update_step']['shipping_method'] = $this->_getShippingMethodsHtml('none');
        } else {
            $result['update_step']['shipping_method'] = $this->_getShippingMethodsHtml();
        }
    }

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

Listing 45. The updateShippingMethodsAction method of the OneStep checkout controller, controllers/OnestepController.php, line 296.

The post data is an array with the following possible elements:

  • “billing” – Serialized data of the “New address” form of the billing address section
  • “shipping” – Serialized data of the “New address” form of the shipping address section
  • “billing_address_id” – The ID of an existing customer address chosen as the billing address
  • “shipping_address_id” – The ID of an existing customer address chosen as the shipping address

Depending on the customer status (logged-in or not) and actions (entering a new address or selecting an existing one) some of the above elements may be empty. The controller class, however, does not check this: the server-side validation is performed by the checkout model methods saveShipping or saveBilling. The controller forwards the posted data to the checkout model in its method saveAddressData (Listing 46).

protected function saveAddressData($data, $addressId, $type, $response = true)
{
    $type = strtolower($type);

    if ($type != 'shipping' && $type != 'billing') {
        $result = array('error' => 1, 'message' => Mage::helper('checkout')->__('Error saving checkout data'));
        if ($response) {
            $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
            return false;
        } else {
            return $result;
        }
    }
    $method = 'save' . ucwords($type);
    $result = $this->getOnestep()->$method($data, $addressId);

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

Listing 46. Saving address data in the OneStep checkout controller, controllers/OnestepController.php, line 99.

In Listing 46, the $result variable gets a value by calling $this->getOnestep()->$method($data, $addressId). The $method method name is generated by concatenating “save” and the address type name: saveBilling or saveShipping. If saving the address data fails, these methods return an array with an error element and error message; otherwise, an empty array is returned. In case of an error the saveAddressData method converts the $result array into JSON, which is then returned as the controller response. If saving the address data is successful, the controller generates the Shipping Method section update by calling $this->_getShippingMethodsHtml().

protected function _getShippingMethodsHtml()
{
    $layout = Mage::getModel('core/layout');
    $layout->getUpdate()
        ->addHandle('checkout_onestep_shippingmethod')
        ->merge('checkout_onestep_shippingmethod');
    $layout->generateXml();
    $layout->generateBlocks();
    $output = $layout->getOutput();
    return $output;
}

Listing 47. Generating the HTML output for the Shipping Method section, controllers/OnestepController.php, line 129.

In Listing 47, the _getShippingMethodsHtml method uses a Magento layout instance to load the checkout_onestep_shippingmethod handle. I’ve defined this handle in the extension’s layout updates file onestepcheckout.xml (Listing 48).

<!-- defining handle for an ajax shipping method update -->
 <checkout_onestep_shippingmethod>
     <!-- Mage_Checkout -->
     <remove name="right"/>
     <remove name="left"/>
     <block
             type="checkout/onepage_shipping_method_available"
             name="checkout.shipping.methods"
             output="toHtml"
             template="solvingmagento/onestepcheckout/shipping_method/available.phtml"/>
 </checkout_onestep_shippingmethod>

Listing 48. Layout definition for the Ajax update of the Shipping Method section, /app/design/frontend/base/default/layout/solvingmagento/onestepcheckout.xml, line 111.

The handle definition in Listing 48 consists of only the checkout/onepage_shipping_method_available block, which is the same block used by the OnePage checkout. I don’t have to make any modifications to the block class and the custom template I use has only a bit better code styling.

The OneStep controller packs the step update HTML into an array $result['update_step']['shipping_method'] and converts it into JSON before returning it as the response back to the client-side JavaScript. In Listing 41, the handler for the “onSuccess” event of the Ajax update request is the updateMethods method, which the ShippingMethod class inherits from the MethodStep object (see Listings 29 and 31). Upon receiving the step update response, the updateMethods method calls checkout.setResponse(response) to display the update to the customer.

Saving the shipping method

When a customer clicks on one of the options in the Shipping Method step, the checkout JavaScript submits the selection to the OneStep controller. The initialize method of the ShippingMethod class registers an observer for the “click” event of the shipping option radio buttons (Listing 49).

$$('input[name="shipping_method"]').each(
    function (element) {
        Event.observe(
            $(element),
            'click',
            this.saveMethod.bindAsEventListener(this)
        );
    }.bind(this)
);

Listing 49. Registering a “click” event observer on the input elements of the Shipping Method form, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/ShippingMethod.js, line 52.

The saveMethod function that handled this “click” event is shown on Listing 50.

saveMethod: function () {
    var parameters = Form.serialize('co-shipping-method-form');

    if ($('shipping:same_as_billing').checked && checkout.steps.shipping) {
        checkout.steps.shipping.setSameAsBilling(true);
    }

    if (checkout
            && checkout.validateCheckoutSteps(
                ['CheckoutMethod', 'BillingAddress', 'ShippingAddress', 'ShippingMethod']
            )
            ) {
        this.postData(
            this.saveShippingMethodUrl,
            parameters,
            'li div.advice-required-entry-' + this.stepId
        );
    }
}

Listing 50. Saving the Shipping Method selection, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/ShippingMethod.js, line 339.

In Listing 50, the saveMethod method prepares a serialized representation of the Shipping Method form inputs which it passes to the postData method to be posted via Ajax to the OneStep controller. The saveMethod method also populates the shipping address form with the billing address data, if the customer chooses to use the billing address as shipping. Before the data is posted, the checkout instance checks the “CheckoutMethod”, “BillingAddress”, “ShippingAddress” sections which must be valid for the the shipping method selection to be saved correctly.

The OneStep controller function responsible for processing the shipping method selection is shown in Listing 51.

public function saveShippingMethodAction()
{
    if ($this->_expireAjax()) {
        return;
    }
    if ($this->getRequest()->isPost()) {
        $data   = $this->getRequest()->getPost('shipping_method', '');
        $result = $this->getOnestep()->saveShippingMethod($data);
        /*
        $result will have error data if shipping method is empty
        */
        if (!isset($result['error'])) {
            Mage::dispatchEvent(
                'checkout_controller_onepage_save_shipping_method',
                array(
                    'request' => $this->getRequest(),
                    'quote'   => $this->getOnestep()->getQuote()
                )
            );
            $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));

            $result['update_step']['payment_method'] = $this->_getPaymentMethodsHtml();
        }
        //update totals
        $this->getOnestep()->getQuote()->getShippingAddress()->setCollectShippingRates(true);
        $this->getOnestep()->getQuote()->setTotalsCollectedFlag(false)->collectTotals()->save();

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

Listing 51. Method saveShippingMethodAction of the OneStep checkout controller, controllers/OnestepController.php, line 339.

In Listing 51, the saveShippingMethodAction method uses the checkout model to save the posted form data by calling $this->getOnestep()->saveShippingMethod($data). In case of an error, the checkout model returns an array with an “error” element and an error message. If the saving is successful, the saveShippingMethodAction method prepares an HTML update for the sections, which depend on the Shipping Method data: Payment Information and Order Review. To ensure Order Review displaying the correct costs of the selected shipping method, the saveShippingMethodAction method forces the checkout model to recalculate shipping costs and collect the quote totals before rendering an updated Order Review content

Finally, the saveShippingMethodAction method returns the section updates as a JSON response to the checkout JavaScript which will display the new content to the customer.

Step 10: Payment Information

The JavaScript class representing this section – Payment in file Payment.js – has a number of specific features, which I replicated from its OnePage counterpart to ensure compatibility with payment modules that may depend on them. These features include an ability of the Payment class to register and execute functions before and after the init method. In my description of the OnePage checkout, I’ve already explained methods beforeInit, afterInit, addBeforeInitFunction, addAfterInitFunction, and the hashes beforeInitFunc and afterInitFunc. The OneStep Payment class has them too and in the same role. The beforeValidate method is also present and executed by the Checkout class in its method validatePaymentMethod.

The rest of the composition of the Payment class is similar to the ShippingMethod class. Just as in the case of the latter, customer can explicitly update the payment options list by clicking the “Update payment options” link (Listing 52).

/**
 * Load methods when user clicks this element
 */
Event.observe(
    $('reload-payment-method-button'),
    'click',
    this.getMethods.bindAsEventListener(this)
);

Listing 52. Registering a “click” event observer on the “reload-payment-method-button” element in the initialize method, /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Payment.js, line 41.

The getMethods function works the same way as its ShippingMethod counterpart by submitting the data of the previous sections and retrieving an updated list of payment options (Listing 53).

getMethods: function () {
    var request,
        parameters = {},
        valid      = false;

    if ($('shipping:same_as_billing').checked && checkout.steps.shipping) {
        checkout.steps.shipping.setSameAsBilling(true);
    }

    /**
     * Validate previous steps, excluding shipping method and payment method
     */
    if (checkout) {
        valid = checkout.validateCheckoutSteps(
            ['CheckoutMethod', 'BillingAddress', 'ShippingAddress', 'ShippingMethod']
        );
    }

    if (valid) {
        this.startLoader();

        parameters =  Form.serialize('co-billing-form') +
            '&' + Form.serialize('co-shipping-form') +
            '&' + Form.serialize('co-shipping-method-form');

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

Listing 53. Posting the checkout data to the controller to update the payment options section /skin/frontend/base/default/js/solvingmagento/onestepcheckout/classes/Payment.js, line 64.

The Payment Information section update is returned by the controller method updatePaymentMethodsAction (Listing 54).

public function updatePaymentMethodsAction()
{
    if ($this->_expireAjax()) {
        return;
    }
    $post   = $this->getRequest()->getPost();
    $result = array('error' => 1, 'message' => Mage::helper('checkout')->__('Error saving checkout data'));

    if ($post) {

        $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', '');

        if ($this->saveAddressData($billing, $billingAddressId, 'billing') === false) {
            return;
        }

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

        $result = $this->getOnestep()->saveShippingMethod($shippingMethod);

        if (!isset($result['error'])) {
            $result['update_step']['payment_method'] = $this->_getPaymentMethodsHtml();
        }
    }
    $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
}

Listing 54. Method updatePaymentMethodsAction of the OneStep checkout controller, controllers/OnestepController.php, line 374.

In Listing 54, the OneStep checkout controller saves the posted address and shipping method data to the quote using a checkout model instance. The HTML update is rendered by the _getPaymentMethodsHtml that loads a layout update for the checkout_onestep_paymentmethod handle (Listing 55).

<checkout_onestep_paymentmethod>
    <remove name="right"/>
    <remove name="left"/>

    <block
            type="checkout/onepage_payment_methods"
            name="checkout.payment.methods"
            output="toHtml"
            template="solvingmagento/onestepcheckout/payment/methods.phtml">
        <action method="setMethodFormTemplate">
            <method>purchaseorder</method>
            <template>payment/form/purchaseorder.phtml</template>
        </action>
    </block>
</checkout_onestep_paymentmethod>

Listing 55. Layout definition for the Ajax update of the Payment Information section, /app/design/frontend/base/default/layout/solvingmagento/onestepcheckout.xml, line 123.

The block responsible for the rendering of the payment methods is the same as in the OnePage checkout. However, I have to use a custom template for it. The contents of the OneStep template is nearly the same as in its OnePage version apart the inline JavaScript (Listing 56 and Listing 57).

<script type="text/javascript">
    //<![CDATA[
    <?php echo $this->getChildChildHtml('scripts'); ?>
    payment.init();
    <?php if (is_string($oneMethod)): ?>
    payment.switchMethod('<?php echo $oneMethod ?>');
    <?php endif; ?>
    //]]>
</script>

Listing 56. The inline JavaScript used in the OnePage payment methods template, /app/design/frontend/base/default/template/checkout/onepage/payment/methods.phtml, line 60.

In the OnePage version of the payment methods template, the JavaScript executes the payment instance’s init and switchMethod functions. In the OneStep checkout, the payment instance is not yet initialized when this template is first rendered. I had to move these calls to the onestep.js file (see Listing 18). The OneStep checkout payment methods template now only sets a JavaScript variable indicating what method the payment instance must switch to later in the onestep.js file.

<script type="text/javascript">
    //<![CDATA[
    <?php echo $this->getChildChildHtml('scripts'); ?>
    <?php if (is_string($oneMethod)): ?>
    var switchToPaymentMethod = '<?php echo $oneMethod ?>';
    <?php endif; ?>
    //]]>
</script>

Listing 57. The inline JavaScript in the payment methods template of the OneStep checkout, /app/design/frontend/base/default/template/solvingmagento/onestepcheckout/payment/methods.phtml, line 70.

The saving of the Payment Information form data is initiated by the interaction in the Order Review section, which I will explain in the next post which will be the final part of this tutorial: Part4: Order Review.

2 thoughts on “OneStep Checkout – A Magento Tutorial, Part 3 (Steps 8 – 10 of 12)

  1. Good day!
    I can’t understand why in “Solvingmagento_OneStepCheckout_Block_Onestep_Billing” function have two “return”:
    return $html and return $select->getHtml(). As i understand function every times will return just $html. It’s right?

  2. Hello, I have been trying to implement your solution but have problems with Paypal. Apparently, there is no token generated for the redirect to paypal. Is there any solution for this problem? Any help would be appreciated.
    thanks

Leave a Reply

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

Theme: Esquire by Matthew Buchanan.