Is it possible to collect credit card info and address info on separate pages in Commerce 2? I want my checkout flow to look like this:


Users enter their credit card information and press "Next."
Users enter their address information (customer profile), then press "Next." 


In the default Commerce workflow, users enter their credit card info and address on the same page.

I want to split credit card info and address info into different pages.

How can I do this safely?

Why do this?

Because if a user makes an error when typing their address and validation fails, it also resets the credit card information.  I want to have the user do the credit card and address info separately so if validation fails, they only have to re-input one or the other.
Creating a checkout flow plugin may solve your problem. Create a Plugin in your custom module like an example shown below:



namespace Drupal\my_checkout_flow\Plugin\Commerce\CheckoutFlow;

use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * @CommerceCheckoutFlow(
 *  id = "custom_checkout_flow",
 *  label = @Translation("Custom checkout flow"),
 * )
 */
class CustomCheckoutFlow extends CheckoutFlowWithPanesBase {

  /**
   * {@inheritdoc}
   */
  public function getSteps() {
    return [
      'login' =&gt; [
        'label' =&gt; $this-&gt;t('Login'),
        'previous_label' =&gt; $this-&gt;t('Return to login'),
        'has_order_summary' =&gt; FALSE,
      ],
      'card_info' =&gt; [
        'label' =&gt; $this-&gt;t('Card information'),
        'previous_label' =&gt; $this-&gt;t('Return to Card information'),
        'next_label' =&gt; $this-&gt;t('Continue to Card information'),
        'has_order_summary' =&gt; FALSE,
      ],
      'address_information' =&gt; [
        'label' =&gt; $this-&gt;t('Address information'),
        'previous_label' =&gt; $this-&gt;t('Return to Address information'),
        'next_label' =&gt; $this-&gt;t('Continue to Address information'),
        'has_order_summary' =&gt; FALSE,
      ],
      'review' =&gt; [
        'label' =&gt; $this-&gt;t('Review'),
        'has_order_summary' =&gt; TRUE,
      ],
    ] + parent::getSteps();
  }

}


After creating a plugin, you need to change the checkout flow settings for your order type.

For more information visit: https://docs.drupalcommerce.org/commerce2/developer-guide/checkout/create-custom-checkout-flow



Update

CustomCheckoutFlow

&lt;?php

namespace Drupal\my_checkout_flow\Plugin\Commerce\CheckoutFlow;

use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase;

/**
 * @CommerceCheckoutFlow(
 *  id = "custom_checkout_flow",
 *  label = @Translation("Custom checkout flow"),
 * )
 */
class CustomCheckoutFlow extends CheckoutFlowWithPanesBase {

  /**
   * {@inheritdoc}
   */
  public function getSteps() {
    return [
      'login' =&gt; [
        'label' =&gt; $this-&gt;t('Login'),
        'previous_label' =&gt; $this-&gt;t('Go back'),
        'has_sidebar' =&gt; FALSE,
      ],
      'card_information' =&gt; [
        'label' =&gt; $this-&gt;t('Card information'),
        'previous_label' =&gt; $this-&gt;t('Go back'),
        'has_sidebar' =&gt; TRUE,
      ],
      'order_information' =&gt; [
        'label' =&gt; $this-&gt;t('Order information'),
        'has_sidebar' =&gt; TRUE,
        'next_label' =&gt; $this-&gt;t('Continue to Order information'),
        'previous_label' =&gt; $this-&gt;t('Go back'),
      ],
      'review' =&gt; [
        'label' =&gt; $this-&gt;t('Review'),
        'next_label' =&gt; $this-&gt;t('Continue to review'),
        'previous_label' =&gt; $this-&gt;t('Go back'),
        'has_sidebar' =&gt; TRUE,
      ],
    ] + parent::getSteps();
  }

}


Then create CheckoutPanes as shown below:

BillingInformation

&lt;?php

namespace Drupal\my_checkout_flow\Plugin\Commerce\CheckoutPane;

use \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\BillingInformation;

/**
 * Provides the billing information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_billing_information",
 *   label = @Translation("Billing information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class CustomBillingInformation extends BillingInformation {

}


PaymentInformation

&lt;?php
namespace Drupal\my_checkout_flow\Plugin\Commerce\CheckoutPane;

use Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentInformation as PaymentInformationBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;

/**
 * Provides the payment information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_commerce_payment_information",
 *   label = @Translation("Custom Payment information"),
 *   display_label = @Translation("Payment information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class PaymentInformation extends PaymentInformationBase {

  /**
   * {@inheritdoc}
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &amp;$complete_form) {
    $pane_form = parent::buildPaneForm($pane_form, $form_state, $complete_form);

    // Add an after build callback in order to make modifications on the address form.
    $pane_form['#after_build'][] = [$this, 'paneFormAfterBuild'];

    return $pane_form;
  }

  /**
   * After build callback for the pane form.
   */
  public function paneFormAfterBuild(array $pane_form, FormStateInterface $form_state) {
    // Get billing form element. Where it is located depends on the payment method that is chosen.
    if (isset($pane_form['add_payment_method']['billing_information'])) {
      $billing_form = &amp;$pane_form['add_payment_method']['billing_information'];
    }
    elseif (isset($pane_form['billing_information'])) {
      $billing_form = &amp;$pane_form['billing_information'];
    }
    else {
      // No billing information found.
      return $pane_form;
    }

    // Get the address form element.
    $address_form = &amp;$billing_form['address']['widget']['0']['address'];

    // Add element validation callback to autofill the address.
    $billing_form['#element_validate'] = array_merge(
      [[$this, 'profileSelectValidate']],
      \Drupal::service('element_info')-&gt;getInfoProperty('commerce_profile_select', '#element_validate', [])
    );

    // Set all address fields to non-required.
    foreach (Element::children($address_form) as $key) {
      $address_form[$key]['#required'] = FALSE;
    }

    // Hide the address form.
    $address_form['#access'] = FALSE;

    return $pane_form;
  }

  /**
   * Element validation callback for the profile select element.
   */
  public function profileSelectValidate(array &amp;$element, FormStateInterface $form_state) {
    $billing_profile = $this-&gt;order-&gt;getBillingProfile()-&gt;get('address')-&gt;getValue();
    $form_state-&gt;setValue($element['address']['widget'][0]['address']['#parents'], $billing_profile[0]);
  }

}


PaymentProcess

&lt;?php

namespace Drupal\my_checkout_flow\Plugin\Commerce\CheckoutPane;

use Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentProcess as BasePaymentProcess;

/**
 * Provides the payment process pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_payment_process",
 *   label = @Translation("Custom Payment process"),
 *   default_step = "payment",
 *   wrapper_element = "container",
 * )
 */
class PaymentProcess extends BasePaymentProcess {

  /**
   * {@inheritdoc}
   */
  public function isVisible() {
    // This pane can't be used without the PaymentInformation pane.
    $payment_info_pane = $this-&gt;checkoutFlow-&gt;getPane('custom_commerce_payment_information');
    return $payment_info_pane-&gt;isVisible() &amp;&amp; $payment_info_pane-&gt;getStepId() != '_disabled';
  }

}


After this set checkout panes for your custom checkout flow as shown in the image below:



Find full code at https://github.com/patilvishalvs/my_checkout_flow