Custom checkout processes with Drupal Commerce 2

Sascha Grossenbacher
https://slides.com/saschagrossenbacher/commerce

1. Start Checkout

2. Checkout

3. Processing

1. Start Checkout

Products with Cart

  • Default Behavior of Commerce 2
  • Product is added to cart, message is shown
  • User goes to cart, confirms again and starts checkout
  • Useful when users might want to buy multiple products

 

Products without Cart

 

Checkout without products

  • For custom processes like donations, dynamic membership/subscription fees or buying products without a product page
  • Requires custom code

 

Code examples

Step 1: Create Order Item

// With a product:
$order_item = OrderItem::create([
  'type' => 'default',
  'purchased_entity' => $commerce_product->getDefaultVariation()->id(),
  'quantity' => 2,
]);

// Arbitrary amount and title
order_item = OrderItem::create([
  'type' => 'custom_item_type',
  'title' => 'Subscription fee',
  'unit_price' => [
    'number' => 50,
    'currency_code' => 'CHF',
  ],
  'quantity' => 1,
  ''
]);

Step 2: Create Order And start checkout

$store = \Drupal::service('commerce_store.current_store')->getStore();
$order = Order::create([
  'type' => 'default',
  'mail' => $this->currentUser()->getEmail(),
  'uid' => $this->currentUser()->id(),
  'ip_address' => \Drupal::request()->getClientIp(),
  'store_id' => $store->id(),
  'order_items' => [$order_item],
  'cart' => FALSE,
]);
$order->save();

// In a controller.
return $this->redirect('commerce_checkout.form', ['commerce_order' => $order->id()]);

// In a form submit callback.
$form_state->setRedirect('commerce_checkout.form', ['commerce_order' => $order->id()]);

Anonymous Checkout

$cart_session = \Drupal::service('commerce_cart.cart_session');
$cart_session->addCartId($order->id(), CartSessionInterface::COMPLETED);
  • Anonymous users require cart session to access order
    • Add order as a completed cart, to
      allow checkout access without showing up as a cart

 

2. Checkout customization

Types & Plugins

  • Almost every concept in Commerce 2 can have multiple types/variants
    • Products and Variants
    • Orders and Order Items
    • Checkout flows
    • Shops and Shop Types
  • Allows for different configuration and custom code
    to act on certain types/plugins

 

Connections

  • The Product Type defines the Variation Type
  • A Variation Type defines the Order Item Type
  • The Order Item Type defines the Order Type
  • The Order Type defines the Checkout Flow
  • A Checkout Flow has a Checkout Flow Plugin

 

 

Connections #2

  • A Checkout Flow Plugin defines the available Checkout Steps (Pages)
    • Display is up to the Plugin, must not be an actual page, e.g. Sidebar Step
  • The Checkout Flow entity assigns Panes to Steps and stores their configuration
  • Checkout Panes are plugins

Customization examples

Customize Checkout steps

/**
 * @CommerceCheckoutFlow(
 *   id = "custom_multistep",
 *   label = "Multistep - Custom",
 * )
 */
class CustomMultiStep extends MultistepDefault {

  /**
   * {@inheritdoc}
   */
  public function getSteps() {
    $steps = parent::getSteps();
    $steps['complete']['label'] = $this->t('Thanks for subscribing!');
    return $steps;
  }

// Alternatively switch out the default class with yours.

function custom_commerce_checkout_flow_info_alter(&$definitions) {
  $definitions['multistep_default']['class'] = MultiStepCustom::class;
}

Custom Checkout Panes

/**
 * @CommerceCheckoutPane(
 *   id = "points_payment",
 *   label = @Translation("Points payment"),
 *   default_step = "_sidebar",
 *   wrapper_element = "container",
 * )
 */
class PointsPayment extends CheckoutPaneBase {

  /**
   * {@inheritdoc}
   */
  public function buildPaneForm(array $pane_form, 
              FormStateInterface $form_state, array &$complete_form) {

    // ...
    return $pane_form;
  }

}

Define custom processes or sublcass and repace default panes to customize them

Hide most blocks during checkout

function custom_block_access(Block $block, $operation, AccountInterface $account) {
  $route_match = \Drupal::routeMatch();
  if ($route_match->getRouteName() == 'commerce_checkout.form' 
    && $route_match->getParameter('commerce_order') instanceof OrderInterface) {
    $order = $route_match->getParameter('commerce_order');

      // Hide blocks in certain regions during checkout (show on complete page).
      if ($order->getState()->getId() == 'draft') {
        $config = \Drupal::configFactory()->get('custom.settings');
        $regions = $config->get('checkout_empty_regions');
        $whitelisted_blocks = $config->get('checkout_whitelisted_blocks');
        if (in_array($block->getRegion(), $regions) 
          && !in_array($block->id(), $whitelisted_blocks)) {
          return AccessResult::forbidden()->addCacheContexts(['url.path']);
        }
      }

    }
  }
}

3. Processing

React to paid orders

  • Event Subscriber on OrderEvents::ORDER_PAID
    • Requires payment gateway that process a payment
      transaction and confirm it as paid.
  • Recommendation: Use a queue for slow operations

Using a workflow

  • Use a Workflow state to handle more complex processes. Can be a mix of manual and automated steps.
  • Commerce uses the State Machine project, workflows are defined in a yourmodule.workflows.yml file
  • Events can then listen to state changes, do something and update the state

 

public function enqueueOrder(WorkflowTransitionEvent $transition_event) {
    $order = $transition_event->getEntity();
    if ($order instanceof OrderInterface && 
        $order->getState()->getWorkflow()->getId() == 'order_aboware' 
        && $transition_event->getTransition()->getToState()->getId() == 'needs_sync') {

Examples

Questions?