A locked order shouldn't disappear from a customer's point of view

Created on 11 April 2019, almost 6 years ago
Updated 7 January 2025, about 2 months ago

Currently I feel this is big UX issue in Commerce and also a checkout conversion rate issue, it was in Commerce 1 and it still is in Commerce 2, at least for the way we use Commerce.

Almost all of our payment methods are offsite and currently if you do anything other than complete your payment in an offsite payment method you are at the risk of losing your current checkout progress as well as your cart with no way to get either of them back. For example the most basic and default action for anyone in a web browser is the back button and if you click it when you're in an offsite payment terminal then you return to the Drupal site, but with no trace of your checkout ever taking place and your cart is gone forever. My gripe with this is that everything is still there, it's only hidden behind a simple locked flag, but from a customer's point of view is essentially the same as the order being destroyed.

My proposed fix for this is to let customers see that their cart is still there even if their order is locked, but not let them change anything. Present them with a clear and understandable message which explains that this cart is currently being checked out and cannot be changed, but also present them with the option of unlocking their cart at the expense of not being able to complete their current checkout. I don't know off-hand how all of this would be made bullet proof in praxis, but I imagine you would need some way of ensuring the current checkout was invalidated and couldn't be completed, maybe through checking for the correct tokens or whatnot. Either way I imagine this scenario would be the edge case here and the normal scenario would be customers who just accidentally lost their carts due clicking the back button in the payment step and just want their cart back so they can checkout again.

I also realize that there may be those who prefer the current workflow as I imagine that currently you can create and checkout multiple orders at the same time in multiple different browser tabs, because whatever you do in one tab doesn't affect the order you currently have in the payment step in another tab, which is why this would probably need to be presented as an alternative to the default workflow.

My plan is to attempt and solve this one way or another either through contrib modules, custom modules or custom patches, but obviously it would be cool if this could be made a part of Commerce. Either way I want to discuss it and maybe hear if anyone can see any pitfalls I haven't thought of.

Feature request
Status

Active

Version

2.0

Component

Cart

Created by

🇳🇴Norway TwiiK Trondheim

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇫🇷France nicolas bouteille

    Hello,

    I totally agree with this. We are about to use Stripe Checkout on our site which means customers will leave our site right after cart validation and Stripe will handle billing information and payment steps.
    We need to lock the order as soon as the customer leaves our site to make sure he cannot update the order once we've communicated the price to be paid to Stripe.
    But for the reasons explained above, it can happen that the customer comes back on our website on the cart step.
    As mentioned above, the default behavior is to hide locked carts so getting back to /cart will show an empty cart. From this point, adding something to his cart will create a new order while the first order still exists and can be paid.
    We too did not like this behavior so here is what we did:

    We patched CartProvider.php and commented out:

    //if ($cart->isLocked()) {
      //continue;
    //}
    

    Now when the cart is locked, it still shows up in /cart
    This means customers can now interact on a locked cart and change the order, so we had to reinforce the code to make sure that never happens.

    In CartManager.php, functions emptyCart(), addOrderItem(), removeOrderItem() and updateOrderItem() need to be updated to do nothing (return;) if the $cart->isLocked
    For the record, for those who would like to do it on their site while this is not in core, this time, to make it easier to maintain, we did not create a patch, we created a custom class MyModuleCartManager.php that extends CartManager and that overrides only the functions mentioned above. For example:

      public function emptyCart(OrderInterface $cart, $save_cart = TRUE) {
        if ($cart->isLocked()) {
          return;
        }
        parent::emptyCart($cart, $save_cart);
      }
    

    For this custom class to replace CartManager, we created a custom ServiceProvider that extends ServiceProviderBase as explained here:
    https://www.drupal.org/docs/drupal-apis/services-and-dependency-injectio...
    Be aware to name it exactly in PascalCase MyModuleNameServiceProvider and place it in my_module/src/.
    This is what the code looks like:

    class MyModuleNameServiceProvider extends ServiceProviderBase {
      public function alter(ContainerBuilder $container) {
        $cart_manager = $container->getDefinition('commerce_cart.cart_manager');
        $cart_manager->setClass(MyModuleCartManager::class);
      }
    }
    

    We also made sure to visually remove the buttons on the cart form that would allow to remove cart items or update quantities...

    We also had to do the same for Coupons.
    In CouponRedemption.php buildInlineForm() needs to hide the coupon form if the order is locked and validateInlineForm() must stop and show an error in case a coupon is submitted but the order has been locked since.
    For this, we also created a custom class MyModuleCouponRedemption.php but this time we used hook_commerce_inline_form_info_alter to replace CouponRedemption with our class:

    function my_module_commerce_inline_form_info_alter(&$definitions, $b = null, $c = null, $d = null) {
      if (isset($definitions['coupon_redemption'])) {
        $definitions['coupon_redemption']['class'] = MyModuleCouponRedemption::class;
      }
    }
    

    So now the customer can see his locked cart, but cannot interact with it anymore. We needed to explain to him what was happening.
    When the customer is at /cart and his cart is locked, we now display a warning message that says the order is locked and cannot be updated anymore because its price needs to remain what's been communicated to the payment partner.
    We also give the opportunity to the customer to definitely cancel this order. So we added a custom button that will cancel the Stripe PaymentIntent and the order. Then the customer sees an empty cart.
    The custom code here is added in views-view--commerce_cart_form.html.twig based on new variables passed through hook_preprocess_views_view__commerce_cart_form

    I understand that this Cancel order button is probably where and why Commerce Core is going to have some trouble implementing all this. Because in OffsitePaymentGatewayInterface there is no function cancelRemotePayment yet. So Commerce Core probably does not have a way to cancel the offsite payment of an order. So if you cannot make sure to properly cancel the order with the remote payment intent as well, you'd better hide the locked cart and allow the customer to start a new cart.
    Maybe this cancel button should be visible only under some conditions like the PaymentGateway is not Offsite or the function cancelRemotePayment is implemented…

  • 🇺🇸United States Erik_MC

    Commenting to mention that we are experiencing the same issue on a client site of mine and interested in exploring a solution so that the end user doesn't think their cart is gone.

Production build 0.71.5 2024