Creating a custom checkout pane for Drupal Commerce in Drupal 8

Parent Feed: 

In this article, I'll show how you can create a custom checkout pane for Drupal Commerce in Drupal 8. For this purpose, we'll create a checkout pane with a configuration form and the ability for users to add coupons to their order.

Creating a custom checkout pane

Checkout pane needs to be created with the correct annotation and in the correct namespace. We'll create CommerceCoupons class in module_name/src/Plugin/Commerce/CheckoutPane directory with annotation like in code below.

namespace Drupal\module_name\Plugin\Commerce\CheckoutPane;
 
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Provides the coupons pane.
 *
 * @CommerceCheckoutPane(
 *   id = "coupons",
 *   label = @Translation("Redeem Coupon"),
 *   default_step = "order_information",
 * )
 */
class CommerceCoupons extends CheckoutPaneBase implements CheckoutPaneInterface {
 
 
/**
 * {@inheritdoc}
 */
public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager) {
  parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager);
}

This means that our custom checkout pane will have an id 'coupons' and will be labelled as Redeem Coupon and will be set on order information step by default.

First, we'll create the settings (configuration) form for our pane. For this purpose, we'll have settings if we, for example, want to set up that user has the ability to redeem only one coupon on order. For this, we'll have to implement four functions.

First, we'll setup our default configuration.

/**
 * {@inheritdoc}
 */
public function defaultConfiguration() {
  return [
      'single_coupon' => FALSE,
    ] + parent::defaultConfiguration();
}

Then we'll implement summary for our configuration form.

/**
 * {@inheritdoc}
 */
public function buildConfigurationSummary() {
  $summary = !empty($this->configuration['single_coupon']) ? $this->t('Single Coupon Usage on Order: Yes') : $this->t('Single Coupon Usage on Order: No');
  return $summary;
}

Next, we'll build our configuration form.

/**
 * {@inheritdoc}
 */
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
  $form = parent::buildConfigurationForm($form, $form_state);
  $form['single_coupon'] = [
    '#type' => 'checkbox',
    '#title' => $this->t('Single Coupon Usage on Order?'),
    '#description' => $this->t('User can enter only one coupon on order.'),
    '#default_value' => $this->configuration['single_coupon'],
  ];
 
  return $form;
}

And save these settings on submit.

/**
 * {@inheritdoc}
 */
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
  parent::submitConfigurationForm($form, $form_state);
 
  if (!$form_state->getErrors()) {
    $values = $form_state->getValue($form['#parents']);
    $this->configuration['single_coupon'] = !empty($values['single_coupon']);
  }
}

You can see how this will look like on Checkout flow edit page.

Checkout flow

Next, we'll build our pane form. We'll check our configuration, and if the order has a coupon applied on it and has config setup for the single coupon, won't show form.

/**
 * {@inheritdoc}
 */
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
  $order_has_coupons = $this->order->coupons->referencedEntities();
  if($this->configuration['single_coupon'] && $order_has_coupons){
    return $pane_form;
  }
  $pane_form['coupon'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Coupon'),
    '#default_value' => '',
    '#required' => FALSE,
  ];
  return $pane_form;
}

After that, we'll implement validation for our coupons and check if a valid coupon is provided and if it can be applied to this order.

/**
 * {@inheritdoc}
 */
public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
  $values = $form_state->getValue($pane_form['#parents']);
  if(!empty($values['coupon'])){
    $coupon_code = $values['coupon'];
    /* @var \Drupal\commerce_promotion\Entity\Coupon $commerce_coupon */
    $commerce_coupon = $this->entityTypeManager->getStorage('commerce_promotion_coupon')->loadByCode($coupon_code);
    $valid = false;
    if($commerce_coupon){
      $promotion = $commerce_coupon->getPromotion();
      $valid = $promotion->applies($this->order) ? true : false;
      if($valid){
        foreach($this->order->coupons->referencedEntities() as $coupon){
          if($commerce_coupon->id() == $coupon->id()){
            $form_state->setError($pane_form, $this->t('Coupon already applied to order.'));
          }
        }
      }
    }
    if(!$valid){
      $form_state->setError($pane_form, $this->t('The specified coupon does not apply for this order.'));
    }
  }
}

What is left for us is to save coupon on order and process coupon. For this, we'll use commerce promotion processor.

/**
 * {@inheritdoc}
 */
public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
  $values = $form_state->getValue($pane_form['#parents']);
  if(!empty($values['coupon'])){
    $coupon_code = $values['coupon'];
    $commerce_coupon = $this->entityTypeManager->getStorage('commerce_promotion_coupon')->loadByCode($coupon_code);
    if($commerce_coupon){
      $this->order->coupons[] = $commerce_coupon->id();
      $coupon_order_processor = \Drupal::service('commerce_promotion.promotion_order_processor');
      $coupon_order_processor->process($this->order);
      $this->order->save();
    }
  }
}

We're done and we can test our new checkout pane.

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web