diff --git a/modules/checkout/commerce_checkout.module b/modules/checkout/commerce_checkout.module
index 522ac15..8fc8501 100644
--- a/modules/checkout/commerce_checkout.module
+++ b/modules/checkout/commerce_checkout.module
@@ -43,6 +43,9 @@ function commerce_checkout_theme() {
'commerce_checkout_pane' => [
'render element' => 'elements',
],
+ 'commerce_checkout_completion_registration' => [
+ 'render element' => 'form',
+ ],
];
return $theme;
diff --git a/modules/checkout/src/Event/CheckoutAccountCreateEvent.php b/modules/checkout/src/Event/CheckoutAccountCreateEvent.php
new file mode 100644
index 0000000..8878c43
--- /dev/null
+++ b/modules/checkout/src/Event/CheckoutAccountCreateEvent.php
@@ -0,0 +1,111 @@
+account = $account;
+ $this->order = $order;
+ }
+
+ /**
+ * Gets the created account.
+ *
+ * @return \Drupal\Core\Session\AccountInterface
+ * The account created during checkout.
+ */
+ public function getAccount() {
+ return $this->account;
+ }
+
+ /**
+ * Gets the checkout order.
+ *
+ * @return \Drupal\commerce_order\Entity\OrderInterface
+ * The checkout order.
+ */
+ public function getOrder() {
+ return $this->order;
+ }
+
+ /**
+ * Sets the redirect for redirecting after the account event has finished.
+ *
+ * @param string $route_name
+ * The name of the route
+ * @param array $route_parameters
+ * (optional) An associative array of parameter names and values.
+ * @param array $options
+ * (optional) An associative array of additional options. See
+ * \Drupal\Core\Url for the available keys.
+ *
+ * @return $this
+ */
+ public function setRedirect($route_name, array $route_parameters = [], array $options = []) {
+ $url = new Url($route_name, $route_parameters, $options);
+ return $this->setRedirectUrl($url);
+ }
+
+ /**
+ * Sets the redirect URL for redirecting after the account event has finished.
+ *
+ * @param \Drupal\Core\Url $url
+ * The URL to redirect to.
+ *
+ * @return $this
+ */
+ public function setRedirectUrl(Url $url) {
+ $this->redirect = $url;
+ return $this;
+ }
+
+ /**
+ * Gets the value to use for redirecting after the account event has finished.
+ *
+ * @return \Drupal\Core\Url|null
+ * A redirect url, if set. Null otherwise.
+ */
+ public function getRedirectUrl() {
+ return $this->redirect;
+ }
+
+}
diff --git a/modules/checkout/src/Event/CheckoutEvents.php b/modules/checkout/src/Event/CheckoutEvents.php
new file mode 100644
index 0000000..b766bd7
--- /dev/null
+++ b/modules/checkout/src/Event/CheckoutEvents.php
@@ -0,0 +1,19 @@
+credentialsCheckFlood = $credentials_check_flood;
+ $this->currentUser = $current_user;
+ $this->entityTypeManager = $entity_type_manager;
+ $this->userAuth = $user_auth;
+ $this->clientIp = $request_stack->getCurrentRequest()->getClientIp();
+ $this->eventDispatcher = $event_dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $checkout_flow,
+ $container->get('commerce.credentials_check_flood'),
+ $container->get('current_user'),
+ $container->get('entity_type.manager'),
+ $container->get('user.auth'),
+ $container->get('request_stack'),
+ $container->get('event_dispatcher')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isVisible() {
+ return $this->currentUser->isAnonymous();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
+ // Ensure that there is no user on the site with the same email address.
+ $existing_user = $this->entityTypeManager->getStorage('user')->loadByProperties(['mail' => $this->order->getEmail()]);
+
+ if ($existing_user) {
+ return $pane_form;
+ }
+
+ $pane_form['register'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Account information'),
+ ];
+
+ $pane_form['register']['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Username'),
+ '#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
+ '#description' => $this->t("Several special characters are allowed, including space, period (.), hyphen (-), apostrophe ('), underscore (_), and the @ sign."),
+ '#required' => FALSE,
+ '#attributes' => [
+ 'class' => ['username'],
+ 'autocorrect' => 'off',
+ 'autocapitalize' => 'off',
+ 'spellcheck' => 'false',
+ ],
+ '#default_value' => '',
+ ];
+
+ $pane_form['register']['password'] = [
+ '#type' => 'password_confirm',
+ '#size' => 60,
+ '#description' => $this->t('Provide a password for the new account.'),
+ '#required' => TRUE,
+ ];
+
+ $pane_form['register']['actions'] = ['#type' => 'actions'];
+ $pane_form['register']['actions']['register'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Create my account'),
+ '#name' => 'commerce_checkout_completion_registration_submit',
+ ];
+
+ return [
+ '#theme' => 'commerce_checkout_completion_registration',
+ 'form' => $pane_form,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
+ // Validate the entity. This will ensure that the username and email are in
+ // the right format and not already taken. The pane should only appear for
+ // a non-existent email, but users can modify the email for their account.
+ $account = $this->entityTypeManager->getStorage('user')->create([
+ 'pass' => $form_state->getValue(['completion_registration', 'register', 'password']),
+ 'mail' => $this->order->getEmail(),
+ 'name' => $form_state->getValue(['completion_registration', 'register', 'name']),
+ ]);
+ $form_state->setTemporaryValue('new_account', $account);
+
+ $violations = $account->validate();
+ foreach ($violations->getByFields(['name']) as $violation) {
+ list($field_name) = explode('.', $violation->getPropertyPath(), 2);
+ $form_state->setError($pane_form['form']['register'][$field_name], $violation->getMessage());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
+ /** @var \Drupal\user\User $user */
+ $user = $form_state->getTemporaryValue('new_account');
+ $user->enforceIsNew();
+ $user->activate();
+ $user->save();
+
+ // Load the new user account.
+ /** @var \Drupal\user\UserInterface $account */
+ $account = $this->entityTypeManager->getStorage('user')->load($user->id());
+
+ user_login_finalize($account);
+ drupal_set_message($this->t('Registration successful. You are now logged in.'));
+ $this->order->setCustomer($account);
+
+ // Add the billing profile to the user's address book.
+ $profile = $this->order->getBillingProfile();
+ if ($profile) {
+ $profile->setOwner($account);
+ $profile->save();
+ }
+
+ // Normally advancing steps in the checkout automatically saves the order.
+ // Since this pane occurs on the last step, manual order saving is needed.
+ $this->order->save();
+
+ $this->credentialsCheckFlood->clearAccount($this->clientIp, $account->getAccountName());
+
+ // Notify other modules about the account creation.
+ $event = new CheckoutAccountCreateEvent($account, $this->order);
+ $this->eventDispatcher->dispatch(CheckoutEvents::ACCOUNT_CREATE, $event);
+ // Redirect the user to a different page, if a redirect has been set.
+ if ($url = $event->getRedirectUrl()) {
+ $form_state->setRedirectUrl($url);
+ }
+ }
+
+}
diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/Login.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/Login.php
index a49bf09..eef86f4 100644
--- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/Login.php
+++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/Login.php
@@ -233,7 +233,7 @@ class Login extends CheckoutPaneBase implements CheckoutPaneInterface, Container
$pane_form['guest']['text'] = [
'#prefix' => '
',
'#suffix' => '
',
- '#markup' => $this->t('Proceed to checkout. You can optionally create an account at the end.'),
+ '#markup' => $this->isRegistrationPaneAvailable() ? $this->t('Proceed to checkout. You can optionally create an account at the end.') : $this->t('Proceed to checkout.'),
];
$pane_form['guest']['continue'] = [
'#type' => 'submit',
@@ -292,7 +292,8 @@ class Login extends CheckoutPaneBase implements CheckoutPaneInterface, Container
public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
$values = $form_state->getValue($pane_form['#parents']);
$triggering_element = $form_state->getTriggeringElement();
- switch ($triggering_element['#op']) {
+ $trigger = !empty($triggering_element['#op']) ? $triggering_element['#op']: 'continue';
+ switch ($trigger) {
case 'continue':
return;
@@ -376,7 +377,8 @@ class Login extends CheckoutPaneBase implements CheckoutPaneInterface, Container
*/
public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
$triggering_element = $form_state->getTriggeringElement();
- switch ($triggering_element['#op']) {
+ $trigger = !empty($triggering_element['#op']) ? $triggering_element['#op']: 'continue';
+ switch ($trigger) {
case 'continue':
break;
@@ -397,4 +399,16 @@ class Login extends CheckoutPaneBase implements CheckoutPaneInterface, Container
]);
}
+ /**
+ * Checks if guests can register at the end of the process.
+ *
+ * @return bool
+ * TRUE if guests may create an account.
+ * FALSE otherwise.
+ */
+ protected function isRegistrationPaneAvailable() {
+ $panes = $this->checkoutFlow->getPanes();
+ return !empty($panes['completion_registration']->getConfiguration()['step']) && $panes['completion_registration']->getConfiguration()['step'] != '_disabled';
+ }
+
}
diff --git a/modules/checkout/templates/commerce-checkout-completion-registration.html.twig b/modules/checkout/templates/commerce-checkout-completion-registration.html.twig
new file mode 100644
index 0000000..63997f5
--- /dev/null
+++ b/modules/checkout/templates/commerce-checkout-completion-registration.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+ * @file
+ * A layout for the checkout registration form
+ *
+ * Available variables:
+ * - form: The form.
+ *
+ * @ingroup themeable
+ */
+#}
+
+
+
{%trans%}Create an account?{%endtrans%}
+
{%trans%}Complete your account registration now to be able to access your order information at any time.{%endtrans%}
+
+
+ {{ form }}
+
+
diff --git a/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.info.yml b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.info.yml
new file mode 100644
index 0000000..425ebcd
--- /dev/null
+++ b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.info.yml
@@ -0,0 +1,5 @@
+name: Commerce Checkout Account Create Event Test
+type: module
+description: 'Helper module for testing creating an account after checkout.'
+package: Testing
+core: 8.x
diff --git a/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.service.yml b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.service.yml
new file mode 100644
index 0000000..9671250
--- /dev/null
+++ b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/commerce_checkout_account_create_event_test.service.yml
@@ -0,0 +1,5 @@
+services:
+ commerce_checkout_account_create_event_test.account_create:
+ class: Drupal\commerce_checkout_account_create_event_test\EventSubscriber\AccountCreate
+ tags:
+ - {name: event_subscriber}
diff --git a/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/src/EventSubscriber/AccountCreate.php b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/src/EventSubscriber/AccountCreate.php
new file mode 100644
index 0000000..c670c0f
--- /dev/null
+++ b/modules/checkout/tests/modules/commerce_checkout_account_create_event_test/src/EventSubscriber/AccountCreate.php
@@ -0,0 +1,38 @@
+getAccount();
+
+ // Set a redirect to the user edit page.
+ $event->setRedirect('entity.user.edit_form', [
+ 'user' => $account->id(),
+ ]);
+ }
+
+}
diff --git a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php
index 22cb253..6bbdc37 100644
--- a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php
+++ b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php
@@ -203,7 +203,9 @@ class CheckoutOrderTest extends CommerceBrowserTestBase {
}
/**
- * Tests that you can register from the checkout pane.
+ * Tests that you can register from the login checkout pane.
+ *
+ * This checkout pane usually appears on the "login" step.
*/
public function testRegisterOrderCheckout() {
$config = \Drupal::configFactory()->getEditable('commerce_checkout.commerce_checkout_flow.default');
@@ -285,6 +287,201 @@ class CheckoutOrderTest extends CommerceBrowserTestBase {
}
/**
+ * Tests that you can register after completing the order as a guest.
+ */
+ public function testRegistrationAfterGuestOrderCheckout() {
+ // Enable the completion_registration pane.
+ /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
+ $checkout_flow = $this->container
+ ->get('entity_type.manager')
+ ->getStorage('commerce_checkout_flow')
+ ->load('default');
+ /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow_plugin */
+ $checkout_flow_plugin = $checkout_flow->getPlugin();
+ /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CompletionRegistration $pane */
+ $pane = $checkout_flow_plugin->getPane('completion_registration');
+ $pane->setConfiguration([]);
+ $pane->setStepId('complete');
+ $checkout_flow_plugin_configuration = $checkout_flow_plugin->getConfiguration();
+ $checkout_flow_plugin_configuration['panes']['completion_registration'] = $pane->getConfiguration();
+ $checkout_flow_plugin->setConfiguration($checkout_flow_plugin_configuration);
+ $checkout_flow->save();
+
+ $this->drupalLogout();
+ $this->drupalGet($this->product->toUrl()->toString());
+ $this->submitForm([], 'Add to cart');
+ $cart_link = $this->getSession()->getPage()->findLink('your cart');
+ $cart_link->click();
+ $this->submitForm([], 'Checkout');
+
+ // Checkout as guest.
+ $this->assertCheckoutProgressStep('Login');
+ $this->submitForm([], 'Continue as Guest');
+ $this->assertCheckoutProgressStep('Order information');
+ $this->submitForm([
+ 'contact_information[email]' => 'guest@example.com',
+ 'contact_information[email_confirm]' => 'guest@example.com',
+ 'billing_information[profile][address][0][address][given_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][family_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][organization]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][address_line1]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][postal_code]' => '94043',
+ 'billing_information[profile][address][0][address][locality]' => 'Mountain View',
+ 'billing_information[profile][address][0][address][administrative_area]' => 'CA',
+ ], 'Continue to review');
+ $this->assertCheckoutProgressStep('Review');
+ $this->assertSession()->pageTextContains('Contact information');
+ $this->assertSession()->pageTextContains('Billing information');
+ $this->assertSession()->pageTextContains('Order Summary');
+ $this->submitForm([], 'Complete checkout');
+ $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
+
+ // Assert that the completion_registration checkout pane is shown.
+ $this->assertSession()->pageTextContains('Create an account?');
+ // Register.
+ $this->submitForm([
+ 'completion_registration[register][name]' => 'User name',
+ 'completion_registration[register][password][pass1]' => 'pass',
+ 'completion_registration[register][password][pass2]' => 'pass',
+ ], 'Create my account');
+ // Assert that the account was created successfully.
+ $this->assertSession()->pageTextContains('Registration successful. You are now logged in.');
+
+ // Log out and try to login again with the chosen password.
+ $this->drupalLogout();
+ $accounts = \Drupal::service('entity_type.manager')->getStorage('user')->loadByProperties(['mail' => 'guest@example.com']);
+ $account = reset($accounts);
+ $account->passRaw = 'pass';
+ $this->drupalLogin($account);
+
+ // Checkout again as guest to test account validation.
+ $this->drupalLogout();
+ $this->drupalGet($this->product->toUrl()->toString());
+ $this->submitForm([], 'Add to cart');
+ $cart_link = $this->getSession()->getPage()->findLink('your cart');
+ $cart_link->click();
+ $this->submitForm([], 'Checkout');
+ $this->assertCheckoutProgressStep('Login');
+ $this->submitForm([], 'Continue as Guest');
+ $this->assertCheckoutProgressStep('Order information');
+ $this->submitForm([
+ 'contact_information[email]' => 'guest2@example.com',
+ 'contact_information[email_confirm]' => 'guest2@example.com',
+ 'billing_information[profile][address][0][address][given_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][family_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][organization]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][address_line1]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][postal_code]' => '94043',
+ 'billing_information[profile][address][0][address][locality]' => 'Mountain View',
+ 'billing_information[profile][address][0][address][administrative_area]' => 'CA',
+ ], 'Continue to review');
+ $this->assertCheckoutProgressStep('Review');
+ $this->assertSession()->pageTextContains('Contact information');
+ $this->assertSession()->pageTextContains('Billing information');
+ $this->assertSession()->pageTextContains('Order Summary');
+ $this->submitForm([], 'Complete checkout');
+ $this->assertSession()->pageTextContains('Your order number is 2. You can view your order on your account page when logged in.');
+
+ $this->submitForm([
+ 'completion_registration[register][name]' => '',
+ 'completion_registration[register][password][pass1]' => 'pass',
+ 'completion_registration[register][password][pass2]' => 'pass',
+ ], 'Create my account');
+ $this->assertSession()->pageTextContains('You must enter a username.');
+
+ $this->submitForm([
+ 'completion_registration[register][name]' => 'User name',
+ 'completion_registration[register][password][pass1]' => '',
+ 'completion_registration[register][password][pass2]' => '',
+ ], 'Create my account');
+ $this->assertSession()->pageTextContains('Password field is required.');
+
+ $this->submitForm([
+ 'completion_registration[register][name]' => 'User @#.``^ รน % name invalid',
+ 'completion_registration[register][password][pass1]' => 'pass',
+ 'completion_registration[register][password][pass2]' => 'pass',
+ ], 'Create my account');
+ $this->assertSession()->pageTextContains('The username contains an illegal character.');
+
+ $this->submitForm([
+ 'completion_registration[register][name]' => 'User name',
+ 'completion_registration[register][password][pass1]' => 'pass',
+ 'completion_registration[register][password][pass2]' => 'pass',
+ ], 'Create my account');
+ $this->assertSession()->pageTextContains('The username User name is already taken.');
+ }
+
+ /**
+ * Tests that you can get redirected after registering at the end of checkout.
+ */
+ public function testRedirectAfterRegistrationOnCheckout() {
+ // Enable the test module 'commerce_checkout_account_create_event_test'.
+ $this->container
+ ->get('module_installer')
+ ->install(['commerce_checkout_account_create_event_test']);
+
+ // Enable the completion_registration pane.
+ /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
+ $checkout_flow = $this->container
+ ->get('entity_type.manager')
+ ->getStorage('commerce_checkout_flow')
+ ->load('default');
+ /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow_plugin */
+ $checkout_flow_plugin = $checkout_flow->getPlugin();
+ /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CompletionRegistration $pane */
+ $pane = $checkout_flow_plugin->getPane('completion_registration');
+ $pane->setConfiguration([]);
+ $pane->setStepId('complete');
+ $checkout_flow_plugin_configuration = $checkout_flow_plugin->getConfiguration();
+ $checkout_flow_plugin_configuration['panes']['completion_registration'] = $pane->getConfiguration();
+ $checkout_flow_plugin->setConfiguration($checkout_flow_plugin_configuration);
+ $checkout_flow->save();
+
+ $this->drupalLogout();
+ $this->drupalGet($this->product->toUrl()->toString());
+ $this->submitForm([], 'Add to cart');
+ $cart_link = $this->getSession()->getPage()->findLink('your cart');
+ $cart_link->click();
+ $this->submitForm([], 'Checkout');
+
+ // Checkout as guest.
+ $this->assertCheckoutProgressStep('Login');
+ $this->submitForm([], 'Continue as Guest');
+ $this->assertCheckoutProgressStep('Order information');
+ $this->submitForm([
+ 'contact_information[email]' => 'guest@example.com',
+ 'contact_information[email_confirm]' => 'guest@example.com',
+ 'billing_information[profile][address][0][address][given_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][family_name]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][organization]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][address_line1]' => $this->randomString(),
+ 'billing_information[profile][address][0][address][postal_code]' => '94043',
+ 'billing_information[profile][address][0][address][locality]' => 'Mountain View',
+ 'billing_information[profile][address][0][address][administrative_area]' => 'CA',
+ ], 'Continue to review');
+ $this->assertCheckoutProgressStep('Review');
+ $this->assertSession()->pageTextContains('Contact information');
+ $this->assertSession()->pageTextContains('Billing information');
+ $this->assertSession()->pageTextContains('Order Summary');
+ $this->submitForm([], 'Complete checkout');
+ $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
+
+ // Assert that the completion_registration checkout pane is shown.
+ $this->assertSession()->pageTextContains('Create an account?');
+ // Register.
+ $this->submitForm([
+ 'completion_registration[register][name]' => 'User name',
+ 'completion_registration[register][password][pass1]' => 'pass',
+ 'completion_registration[register][password][pass2]' => 'pass',
+ ], 'Create my account');
+ // Assert that the account was created successfully.
+ $this->assertSession()->pageTextContains('Registration successful. You are now logged in.');
+
+ // Assert that a redirect had taken place.
+ $this->assertUrl('/user/3/edit');
+ }
+
+ /**
* Tests checkout behaviour after a cart update.
*/
public function testCheckoutFlowOnCartUpdate() {