Add new step in Commerce checkout

One of new drupal 7 projects I had the task to add new step in checkout process. In this article I would like to share how easy it is now with Drupal Commerce

First of all lets look to the new API of the commerce checkout. All the pages are defined with hook_commerce_checkout_page_info() (you can see example implementation in commerce_checkout_commerce_checkout_page_info()). Every page consists of the panes that are defined in hook_commerce_checkout_pane_info() (as example please take a look at commerce_checkout_commerce_checkout_pane_info()). So it is very straightforward and clear. We need to add new page and new pane.

So lets add our new custom step. In my task it was to create separate node as one of the steps of checkout.

In order to accomplish this we need to implement hook_commerce_checkout_page_info()

/**
 * Implements hook_commerce_checkout_page_info().
 */
function example_commerce_checkout_page_info() {
  $checkout_pages = array();
 
  $checkout_pages['example_form_page'] = array(
    'name' => t('Example form'),
    'title' => t('Fill the form to proceed with checkout'),
    'weight' => -10,
    'status_cart' => FALSE,
    'buttons' => TRUE,
  );
 
  return $checkout_pages;
}

Then we need to create pane for this page.

/**
 * Implements hook_commerce_checkout_pane_info().
 */
function example_commerce_checkout_pane_info() {
  $checkout_panes = array();
 
  $checkout_panes['example_pane'] = array(
    'title' => t('Node form'),
    'file' => 'example.checkout_pane.inc',
    'base' => 'example_pane',
    'page' => 'example_form_page',
    'callbacks' => array(
      'checkout_form_submit' => 'example_pane_checkout_form_submit',
    ),
    'fieldset' => FALSE,
  );
 
  return $checkout_panes;
}

Then we should create separate file example.checkout_pane.inc where we store form for our pane.

/**
 * Custom checkout pane.
 * 
 * Function name should consist of <pane key>_checkout_form.
 */
function example_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
  global $user;
  $pane_form = array();
 
  // Retrieve article node form.
  $type = 'page';
  $node = array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $type, 'language' => LANGUAGE_NONE);
 
  // Merge empty node with order node data.
  if (isset($order->data['node'])) {
    $node = array_merge($node, $order->data['node']);
  }
 
  $node = (object) $node;
 
  module_load_include('inc', 'node', 'node.pages');
 
  // Retrieve node form.
  $node_form_state = array();
  $node_form_state['build_info']['args'] = array($node);
  $node_form_state += form_state_defaults();
  $pane_form = drupal_retrieve_form($type . '_node_form', $node_form_state);
 
  // Hide some node form elements.
  $pane_form['actions']['submit']['#access'] = FALSE;
  $pane_form['actions']['preview']['#access'] = FALSE;
  $pane_form['author']['#access'] = FALSE;
  $pane_form['options']['#access'] = FALSE;
  $pane_form['revision_information']['#access'] = FALSE;
 
  return $pane_form;
}
 
/**
 * Custom checkout pane submit handler.
 *
 * Save node data to order.
 */
function example_pane_checkout_form_submit($form, &$form_state, $checkout_pane, &$order) {
  $order->data['node'] = $form_state['values'];
}

After retrieve node form (we user "page" content type) we remove some form elements.

In submit handler we save filled data to $order->data property so it goes along with our order to next steps. As a tip at the moment we cannot use dpm() function in submit handler as these messages are not shown, so we can use simple output with print_r() to debug submit handler.

In my case I needed to save the node only after all checkout procedure is finished. For this we can use hook hook_commerce_checkout_router() in following way:

/**
 * Implements hook_commerce_checkout_router().
 *
 * Create node on complete checkout page.
 */
function example_commerce_checkout_router($order, $checkout_page) {
  if ($checkout_page['page_id'] != 'complete' || !isset($order->data['node'])) {
    return;
  }
 
  $node = (object) $order->data['node'];
  node_save($node);
  unset($order->data['node']);
}

So this is it. Now we have custom built step in checkout that works very nicely using several hooks and building custom checkout pane.

I hope you have enjoyed and can see how flexible Drupal commerce is.

You are welcome to download source code of the example module below.

Attached files: 

Comments

This may prove to be very handy in the future. Thanks Yuriy!

Great tutorial, and glad to see our improvements to the checkout system have made it easy for you to make this sort of customization. I also hadn't thought about using hook_commerce_checkout_router() like this. Sneaky. ; ) Given your usage you might also do just fine implementing hook_commerce_checkout_complete(). We may need to document that, but it'll get called when the function commerce_checkout_complete() gets called when the customer is redirected to the Completion page. The function rules_invoke_all() triggers the Rules event and then invokes the hook of the same name. Thanks for sharing! -Ryan, Commerce Guys

Thanks for the feedback!

Your tutorial was extremely helpful, thank you. I have run into a problem using webforms/webform module with this setup. The webforms have their own submit button. How do I adjust the code the webform to use the continue button for validation, so I can move to the next step? Thanks in advance.

want to create a optional step in the checkout...how would I go about this should I use a rule?? I have never used rules before. I was thinking I would put a radio button option on one page and if the user selects yes then the next page shows the form, if not then the next page goes to the final step... thanks in advance.

I believe you should take a look at the code when commerce switch the page and set the next page depending on the value of the radio button.

Your tutorial was extremely helpful, thank you. I have run into a problem using webforms/webform module with this setup. The webforms have their own submit button. How do I adjust the code the webform to use the continue button for validation, so I can move to the next step? Thanks in advance.

I've follwed your lead while trying to save a node type I created myself. Unfortunately it seems that including this in a checkout page, which renders just fine, but when you submit it fails on the checks on float / decimal fields that are part of the node type. Notice: Undefined index: field in field_widget_field() Notice: Undefined index: instance in field_widget_instance() Etc. all seem to point towards field_widget_instance(Array, Array) number.module:370 And looking at the form it keeps telling to use use only number and the decimal separator eventhough we only entered integers. Any idea as to why the checks seem to be failing? The fields contain the correct data.

As I remember, FieldAPI keeps information about fields in $form_state array. So I would try to 'merge' $node_form_state and $form_state arrays after node form retrieved. Another 'hackish' was is to add 'after_build' function to our form and remove/change validation function of the field that throws error.

It seemed that any type of number was trhowing the notices and was not able to validate. We resorted to changing the field types to text fields and handle some extra validation in our custom validation hook. Some testing showed that we were then able to go through all steps without a problem. Thanks very much for your tutorial it was extremely helpful in getting this far!