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() {