Custom Commerce Checkout Panes

We've created a number of paid membership sites at Aten and as we've committed to creating new sites in Drupal 7, I was excited to take advantage of fieldable entities and the new Drupal Commerce system.

Advances in Drupal 7

Fieldable entities allow fields to be applied to nearly anything in Drupal 7, most notably, users. This is particularly important for membership sites, because it allows for an unlimited amount of user information to be stored: names, contact information, membership information - all the information an organization would need to manage relationships with their constituents and members. Similarly, Drupal Commerce has added a number of new features that have made CRM style functionality far easier. Foremost is the integration of Drupal Commerce with Rules, which allows membership purchase by the creation of a rule that runs on the "Completing the checkout process" event, checks for a condition that "Order contains a particular product" and runs the action "Add user role."

One of the most commonly requested features for membership sites is to include member profile information fields in the checkout process. Doing this allows organizations to encourage or require users to enter additional information beyond the standard billing information. Drupal Commerce makes this functionality easy to add with the checkout panes feature.

Creating custom checkout panes

To create a custom checkout pane, you'll first need to tell Drupal Commerce about your pane using hook_commerce_checkout_pane_info() in your module file:

/**
 * Implements hook_commerce_checkout_pane_info()
 */
function my_module_commerce_checkout_pane_info() {
  $panes['my_module'] = array(
    'title' => t('My Module Pane'),
    'page' => 'checkout',
    'weight' => 10,
    'file' => 'includes/my_module.checkout_pane.inc',
    'base' => 'my_module_pane',
  );
  return $panes;
}

The array key "my_module" is used as an identifier for your pane. The title is what is displayed in both the checkout panes settings page and to the user during checkout. The page and weight determine where you pane will appear by default, although you can reorder the pane using the checkout pane settings page drag and drop functionality. The file tells commerce where to find the appropriate pane functions and the base determines the base or root name for the pane functions. This is notable because it's possible to create multiple panes per module, so the pane system cannot assume your pane functions will be the same as your module name (i.e. standard hook_callback() naming conventions).

Now that Drupal Commerce knows about your pane, you'll need to create the pane in the file specified (includes/my_module.checkout_pane.inc). Let's start with a settings form, so that store administrators can customize the pane.

/**
 * Implements base_settings_form()
 */
function my_module_pane_settings_form($checkout_pane) {
  $form['my_module_pane_field'] = array(
    '#type' => 'textfield',
    '#title' => t('My Module Pane Field'),
    '#default_value' => variable_get('my_module_pane_field', ''),
  );
  return $form;
}

The base_settings_form() function receives information about itself through the $checkout_pane parameter and returns a standard Drupal form array. Note that you do not need to add a submit button, since Drupal Commerce will add one automatically. Also, you do not need to add a submit callback, since Drupal Commerce will automatically store your form values using the Drupal variable system.

Now that the backend configuration is handled, let's configure the customer facing part of the pane using base_checkout_form():

/**
 * Implements base_checkout_form()
 */
function my_module_pane_checkout_form($form, $form_state, $checkout_pane, $order) {
  $checkout_form['my_module_pane_field_display'] = array(
    '#markup' => variable_get('my_module_pane_field', ''),
  );
  $checkout_form['my_module_pane_field2'] = array(
    '#type' => 'textfield',
    '#title' => t('My Module Pane Field 2'),
  );
  return $checkout_form;
}

Just like base_settings_form(), base_checkout_form() receives information about itself from $checkout_pane, but since this is displayed during the actual checkout process, it also receives an $order parameter that contains information about the current customer's order. One important thing to note about orders is that they are entities, as are line items and products. This means that you may have to do a bit of work to trace an order back to the line items and back again to the products in order to get information. It also means that the current customer id can be found at $order->uid.

Also like base_settings_form(), base_checkout_form() returns a standard Drupal form array, however the values are not automatically saved by the Drupal variable system, because it's most likely that you'll want to do something more than simply saving the submitted values. base_checkout_form_submit() is the predefined submit callback:

/**
 * Implements base_checkout_form_submit()
 */
function my_module_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
  // do something here with 
  // $form_state['values']['my_module_pane_field2']
}

Since the pane system uses the standard Drupal form API, submitted values can be found in $form_state['values'] and you can do just about anything in the submit callback.

A world of pane

Using the checkout pane framework, we can see how it's easy to add form fields to the checkout process, then use the submitted values to update user account information, but that's only one way to use the pane framework. The Commerce Extra Panes module uses the pane system to display nodes (e.g. Terms & Conditions information) and the Commerce Coupon module adds coupon and discounts handling in panes. There are so many possibilities for Drupal Commerce checkout panes, so leave a comment below and tell us about your idea for a checkout pane.

15 Comments

Thanks for the write-up! I was also happy to find out about the Commerce Fieldgroup Panes module - http://drupal.org/project/commerce_fieldgroup_panes. It lets you add fieldgroups to the basic Order type and expose those fieldgroups to the Checkout system as checkout panes.

Just ran around the Tower of London today at the same time (though in separate groups) as Jon. Sadly, some of those imprisoned there didn't have it as easy with their "pains" as we do with ours. : P

Great explanation!

I found the Commerce Fieldgroup Panes mentioned by Ryan very useful for a recent project.

Can you explain a bit more about how you are adding additional information to the user entity? Are you using a custom form in your pane, embedding some other form, or using a custom customer profile type offered by Commerce?

@avr: After a lot of consideration, we decided to user the Profile2 module for user profiles instead of adding the fields directly to the user entities. This gave us a bit more control over the types of profiles (since different user roles own and can view different profiles.

In my base_checkout_form() function, I simply call field_attach_form(), which adds the profile2 form fields to the checkout form. The beautiful thing is that field_attach_form() preserves the form layout (vertical tabs, fieldsets, etc) and it also prefills the default values from a user's existing profile.

Here's a code snippet

function profile_checkout_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
  ...
  $profiles = profile2_load_by_user($order->uid);
  $profile_form = array('#parents' => array($checkout_pane['pane_id']));
  foreach ($profiles as $bundle => $profile) {
    field_attach_form('profile2', $profile, $profile_form, $form_state);
  }
  ...
  return $profile_form;
}

In my base_checkout_form_submit() I call field_attach_submit() to save the data

Jason,

Thanks for the details - and sharing a bit of that snippet. Very helpful.

I've been going back and forth on a current project with user + fields vs. Profile2 - so thanks for clarifying your decision in that as well.

It seems that this method and a custom checkout page could make event checkout/registration rather simple, too. Maybe not with Profile2 specifically, but with the ability to iterate over products during each checkout step + embedded/attached forms.

How can I set a validate function for the custom chekout_pane. I tried to implement checkout_form_validate(). It is working partialy, now I cannot go to next step even though I have no errors in validate function. Can you give me a hint?

Daniel, How are you implementing checkout_form_validate()? You should be able to set a #validate attribute on your form or #element_validate on individual form fields per the Form API

Why is the information from the custom checkout pane, not inserted on the review pane? I can't review my inserted information. Do i have to manually implement it with the submit function?

How can I set a checkout pane as disabled based on the content of the cart. For example if the cart contains product a, then the additional pane should be displayed otherwise it should be disabled.

thanks from turkey,I've been going back and forth on a current project with user + fields vs. Profile2 - so thanks for clarifying your decision in that as well.

Nice article - very well explained.

You can also validate the form using:

/** * Custom validate handler for checkout forms */ function my_module_pane_checkout_form_validate(&$form, &$form_state){ }

Hello, Is there a way to control the visibility of the pane depending on certain conditions ? I am using the Commerce Fieldgroup Panes module. I have also set up two order types but I would like to have different checkout panes depending on the order type... thanks

Yes, really well written! And while I have my pane and field(s) within showing now, I do not know why my _submit handler is apparently not firing. My module is called "c4c_cards" and the paneId is c4c_cards_pane, so: /** * Implements base_checkout_form_submit() */ function c4c_cards_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) { dpm($form, 'form'); dpm($form_state, 'form state'); dpm($checkout_pane, 'checkout pane'); dpm($order, 'order'); } ought to work, but it seems to gloss right over :( Am I missing something?

Just wrote a minute ago, awaiting comment moderation. Wanted to followup to report that my _submit handler *does* get called when there's a validation error on the Commerce checkout form fields, but I don't see what I can do with the submit pre-checkout-complete.. In my solution, I'm effectively adding a value that the user can add to update a node that is created upon checkout completion. I don't have references to "anything" yet...

Thank you for the great tutorial. I am new to drupal how would I in the pane_checkout_form_submit attach the submitted forum values to the order ?

Hi, regarding custom checkout pane.. Could you give me point of view how to passing line item field value to billing information field? In my case i've created postal code field as a line item field, i want to pass postal code value to post code field in the billing information and make the text field readonly

How to do this?

Add a comment

About the Author

Jason Yee

Developer

Jason joined the Aten team in early 2010 and has a long history of development in a variety of languages and technologies. Most recently, he's developed an affinity for Drupal development. Along with his passion for crafting great code, Jason is obsessed with all aspects of food and food science.

Read More Jason's Blog Posts