craftcms / commerce

Fully integrated ecommerce for Craft CMS.
https://craftcms.com/commerce
Other
226 stars 170 forks source link

Save cart contents in user/customer account for returning visitors #704

Closed srosenbrand closed 5 years ago

srosenbrand commented 5 years ago

Feature request.

When a user adds products to their cart, then logs-out or closes their browser and returns later the cart is empty. In my opinion that's undesirable from an UX and revenue point of view. Of course after a cart is completed and became an order the cart should be empty.

Carts are saved in the CP as inactive carts, so what is the use for this when users can not acces this data?

Steps to reproduce

  1. Login as customer (or even guest I suppose)
  2. Throw some goodies in the cart
  3. Close browser or log-out
  4. Have a good night sleep
  5. Decide to buy those goodies anyway
  6. Be disappointed to find an empty cart
  7. Do it all over again...

Additional info

bossanova808 commented 5 years ago

Yep I also preferred the Commerce 1 behaviour here - I (and, it seems, our customers) like carts to survive pretty much no matter what. That said, for privacy reasons, it might be worth ditching any address info on the cart...I think that is folks' fears with persistent carts maybe? I don't know, TBH - I like persistent ones and figure people should use incognito or whatevs if they are on public PCs...

echantigny commented 5 years ago

I agree with this request. If we take your example, a lot of people that were on the fence won't go through step 7 again, they will just decide not to buy. This really needs to happen.

lukeholder commented 5 years ago

Looks like we will need to move back to using an independent cookie.

echantigny commented 5 years ago

@lukeholder Great news!!

I'm sure you thought of these scenarios, but I'm gonna put them here just in case:

srosenbrand commented 5 years ago

This is great!

In addition to what Eric mentioned above, could you consider storing cart data in a user/consumer account for ultimate UX when switching devices. It’s really not that uncomen for users to browse on mobile and really convert and checkout on desktop for example.

echantigny commented 5 years ago

I agree there too. I kind of assumed that if we have persistant carts, it would check if a cart exists when you log in.

bossanova808 commented 5 years ago

Just chiming in - yep, the best implementations are where if they are a user, and logged in, the cart is saved persistently on that user (for some months at least - via Google we know folks will often convert anywhere up to 90 days or so from initial contact, and sometimes even longer).

...And I know, literally from seconds ago, that my wife gets annoyed even if, as just a guest browsing a site, her cart from a few days ago has now disappeared. Basically, people like to keep their behaviour DRY ;)

lukeholder commented 5 years ago

This will be a breaking change for 3rd party plugins like this: https://github.com/mediabeastnz/craft-commerce-abandoned-cart/blob/master/src/controllers/BaseController.php#L122

So need to make the change on a branch and notify 3rd party plugins for testing before releasing.

eric-chantigny commented 5 years ago

@lukeholder I'm guessing that a plugin like that actually introduces the Core functionality that was missing. It's ok of course to get them to test, but I really hope it won't be long to get to the release of the functionality.

Thanks Luke

echantigny commented 5 years ago

@lukeholder Hi Luke! Any update on the state of this feature? Thanks

RitterKnightCreative commented 5 years ago

Been playing around with this a bit on a dev install.

For authenticated users, I think it could be as simple as something like:


          Event::on(
             \yii\web\User::class,
             \yii\web\User::EVENT_AFTER_LOGIN,
            function (\yii\web\UserEvent $event) {

                $commerce = craft\commerce\Plugin::getInstance();
                $num = $commerce->getCarts()->getCart()->id;

                // only do something if the cart is empty;  if cart is empty, id will be null
                if (!$num) {

                    $userEmail = ($event->identity->email);        
                    $query = Order::find();
                    $query->email($userEmail);
                    $query->isCompleted(false);
                    $query->orderBy('dateCreated DESC');
                    $order =  $query->one();

                 // set the new cart to the last one the user updated

                    if ($order) {
                        $commerce->carts->forgetCart();
                        $cartNumber = $order->number;
                        $session = Craft::$app->getSession();
                        $session->set('commerce_cart', $cartNumber);
                    }

                } else {
                     // cart has items in it, Craft will use it instead
                     // might want to do something there like show a flash, etc.                     
                } 
            });

Basically on login, if there's an active cart, Craft will use that cart instead but if the cart is empty (eg maybe the user is logging in to revisit their prior session), it'll grab the most recent (inactive) cart they had and make that the most recent cart. (Much like how an abandoned cart notification works.)

I would highly recommend that Commerce not merge cart items. This is actually the default behavior in Magento and other platforms and a lot of devs turn it off/work around it because what they found was users forget they had items in their authenticated cart prior to logging in. You end up creating more work for the user. Ryan Szrama of Drupal Commerce/Ubercart talks about this and his reasoning is correct.

However, since Craft does save every inactive cart, there's nothing stopping us from spitting those old items out on the cart page (or some other page as well). "Hey we noticed you may have left some items behind from earlier...." Loop through the inactive carts and spit out the products the user hasn't already added and offer to add them (similar to how you'd do an upsell).

eric-chantigny commented 5 years ago

@RitterKnightCreative I understand your reasoning behind this and I can see that it might be an option to have for some use cases, but in the case of the site I'm taking care of, people want to have the carts merged. Too often people told us that they added a few items to their carts, thinking they were logged in. Then, went and logged in to checkout to notice that not all of their items were in the cart.

Maybe a simple toggle in the backend to merge or not merge carts on login would take care of all use cases? I think it would be better to have both option in the core to get rid of any unnecessary plugins just to deal with this.

bossanova808 commented 5 years ago

Personally I am with @RitterKnightCreative here - I think only restore an account's cart if the current cart is empty...I consider the current cart as the newest, therefore the best expression of the user's current intenstions. That said, I think some folks do log in looking for their 'saved cart from last time' so I do imn general like that functionality. But I do think you have to detect it happening, and show the user an explicit message and take them to their cart page so that it is very clear/explicit to the user what has been happening....because I definitely agree that items spontaneously being added to a cart without any UI feedback is a big no no with customers.

rob-c-baker commented 5 years ago

This would be great to have. @lukeholder Is there any update on timeline for this feature? Thanks.

lukeholder commented 5 years ago

Update.

On the develop branch right now, when a user logs in, all previous orders are merged (optionally) with the current order (line items are copied into the current order). Next up will be switching back to cookies for the cart.

Change: 4d719296e14077eb5f9ae83d9257803fba09d610

eric-chantigny commented 5 years ago

I'll be testing this today @lukeholder . Thank you

echantigny commented 5 years ago

@lukeholder I tested, and I'm sorry to say that this is not working correctly on my end.

Also, please note that there's also something else wrong on develop: #843

Steps: 1- 'mergePreviousCartsOnCustomerLogin' => true in my commerce config file (I'm assuming this will be a backend setting later? 2- Login and add items to the cart 3- Open an incognito window, login and this error comes up (I didn't add anything to the cart before loging in)

Stack trace:

PDOException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'orderId' cannot be null in C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Command.php:1290
Stack trace:
#0 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Command.php(1290): PDOStatement->execute()
#1 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Command.php(1091): yii\db\Command->internalExecute('INSERT INTO `co...')
#2 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Schema.php(433): yii\db\Command->execute()
#3 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\ActiveRecord.php(600): yii\db\Schema->insert('{{%commerce_lin...', Array)
#4 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\ActiveRecord.php(566): yii\db\ActiveRecord->insertInternal(NULL)
#5 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\BaseActiveRecord.php(678): yii\db\ActiveRecord->insert(false, NULL)
#6 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\services\LineItems.php(211): yii\db\BaseActiveRecord->save(false)
#7 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\elements\Order.php(2133): craft\commerce\services\LineItems->saveLineItem(Object(craft\commerce\models\LineItem), false)
#8 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\elements\Order.php(974): craft\commerce\elements\Order->_saveLineItems()
#9 C:\wamp64\www\my-site\vendor\craftcms\cms\src\services\Elements.php(515): craft\commerce\elements\Order->afterSave(true)
#10 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\helpers\Order.php(81): craft\services\Elements->saveElement(Object(craft\commerce\elements\Order))
#11 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\services\Customers.php(271): craft\commerce\helpers\Order::mergeOrders(Object(craft\commerce\elements\Order), Object(craft\commerce\elements\Order))
#12 [internal function]: craft\commerce\services\Customers->loginHandler(Object(yii\web\UserEvent))
#13 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Event.php(312): call_user_func(Array, Object(yii\web\UserEvent))
#14 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Component.php(636): yii\base\Event::trigger('yii\\web\\User', 'afterLogin', Object(yii\web\UserEvent))
#15 C:\wamp64\www\my-site\vendor\yiisoft\yii2\web\User.php(495): yii\base\Component->trigger('afterLogin', Object(yii\web\UserEvent))
#16 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\User.php(423): yii\web\User->afterLogin(Object(craft\elements\User), false, 0)
#17 C:\wamp64\www\my-site\vendor\yiisoft\yii2\web\User.php(264): craft\web\User->afterLogin(Object(craft\elements\User), false, 0)
#18 C:\wamp64\www\my-site\vendor\craftcms\cms\src\controllers\UsersController.php(168): yii\web\User->login(Object(craft\elements\User), 0)
#19 [internal function]: craft\controllers\UsersController->actionLogin()
#20 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\InlineAction.php(57): call_user_func_array(Array, Array)
#21 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#22 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Controller.php(109): yii\base\Controller->runAction('login', Array)
#23 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Module.php(528): craft\web\Controller->runAction('login', Array)
#24 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(297): yii\base\Module->runAction('users/login', Array)
#25 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(565): craft\web\Application->runAction('users/login', Array)
#26 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(281): craft\web\Application->_processActionRequest(Object(craft\web\Request))
#27 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Application.php(386): craft\web\Application->handleRequest(Object(craft\web\Request))
#28 C:\wamp64\www\my-site\public_html\index.php(21): yii\base\Application->run()
#29 {main}

Next yii\db\IntegrityException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'orderId' cannot be null
The SQL being executed was: INSERT INTO `commerce_lineitems` (`purchasableId`, `orderId`, `taxCategoryId`, `shippingCategoryId`, `options`, `optionsSignature`, `qty`, `price`, `weight`, `width`, `length`, `height`, `snapshot`, `note`, `saleAmount`, `salePrice`, `total`, `subtotal`, `dateCreated`, `uid`, `dateUpdated`) VALUES (528, NULL, 1, 1, '[]', 'd751713988987e9331980363e24189ce', 1, '49.95', '0', '0', '0', '0', '{\"onSale\":false,\"cpEditUrl\":\"#\",\"productFields\":[],\"fields\":[],\"price\":49.95,\"sku\":\"RIO281\",\"description\":\"Caylus\",\"purchasableId\":\"528\",\"options\":[],\"sales\":[]}', '', '0', '49.95', '49.95', '49.95', '2019-05-06 15:48:20', '9d7dee1b-3e89-42a3-a666-4599218c51d7', '2019-05-06 15:48:20') in C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Schema.php:664
Stack trace:
#0 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Command.php(1295): yii\db\Schema->convertException(Object(PDOException), 'INSERT INTO `co...')
#1 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Command.php(1091): yii\db\Command->internalExecute('INSERT INTO `co...')
#2 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\Schema.php(433): yii\db\Command->execute()
#3 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\ActiveRecord.php(600): yii\db\Schema->insert('{{%commerce_lin...', Array)
#4 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\ActiveRecord.php(566): yii\db\ActiveRecord->insertInternal(NULL)
#5 C:\wamp64\www\my-site\vendor\yiisoft\yii2\db\BaseActiveRecord.php(678): yii\db\ActiveRecord->insert(false, NULL)
#6 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\services\LineItems.php(211): yii\db\BaseActiveRecord->save(false)
#7 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\elements\Order.php(2133): craft\commerce\services\LineItems->saveLineItem(Object(craft\commerce\models\LineItem), false)
#8 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\elements\Order.php(974): craft\commerce\elements\Order->_saveLineItems()
#9 C:\wamp64\www\my-site\vendor\craftcms\cms\src\services\Elements.php(515): craft\commerce\elements\Order->afterSave(true)
#10 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\helpers\Order.php(81): craft\services\Elements->saveElement(Object(craft\commerce\elements\Order))
#11 C:\wamp64\www\my-site\vendor\craftcms\commerce\src\services\Customers.php(271): craft\commerce\helpers\Order::mergeOrders(Object(craft\commerce\elements\Order), Object(craft\commerce\elements\Order))
#12 [internal function]: craft\commerce\services\Customers->loginHandler(Object(yii\web\UserEvent))
#13 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Event.php(312): call_user_func(Array, Object(yii\web\UserEvent))
#14 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Component.php(636): yii\base\Event::trigger('yii\\web\\User', 'afterLogin', Object(yii\web\UserEvent))
#15 C:\wamp64\www\my-site\vendor\yiisoft\yii2\web\User.php(495): yii\base\Component->trigger('afterLogin', Object(yii\web\UserEvent))
#16 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\User.php(423): yii\web\User->afterLogin(Object(craft\elements\User), false, 0)
#17 C:\wamp64\www\my-site\vendor\yiisoft\yii2\web\User.php(264): craft\web\User->afterLogin(Object(craft\elements\User), false, 0)
#18 C:\wamp64\www\my-site\vendor\craftcms\cms\src\controllers\UsersController.php(168): yii\web\User->login(Object(craft\elements\User), 0)
#19 [internal function]: craft\controllers\UsersController->actionLogin()
#20 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\InlineAction.php(57): call_user_func_array(Array, Array)
#21 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#22 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Controller.php(109): yii\base\Controller->runAction('login', Array)
#23 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Module.php(528): craft\web\Controller->runAction('login', Array)
#24 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(297): yii\base\Module->runAction('users/login', Array)
#25 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(565): craft\web\Application->runAction('users/login', Array)
#26 C:\wamp64\www\my-site\vendor\craftcms\cms\src\web\Application.php(281): craft\web\Application->_processActionRequest(Object(craft\web\Request))
#27 C:\wamp64\www\my-site\vendor\yiisoft\yii2\base\Application.php(386): craft\web\Application->handleRequest(Object(craft\web\Request))
#28 C:\wamp64\www\my-site\public_html\index.php(21): yii\base\Application->run()
#29 {main}
Additional Information:
Array
(
    [0] => 23000
    [1] => 1048
    [2] => Column 'orderId' cannot be null
)
echantigny commented 5 years ago

@brandonkelly @lukeholder This issue I mentioned just above is still true on 2.1.5.

And just to add more info, this happens only if you start with an empty cart. If there's items in the "guest" cart and you then login, it works fine.

echantigny commented 5 years ago

Pull request added to fix the above. #847

echantigny commented 5 years ago

@lukeholder There's something working weird here and I'm not sure how it could be fixed.

The issue is that it fetches the old carts “On Login”. So, if you do this:

1- Create your cart at home. Stay logged in 2- Login at work and add some stuff to it 3- Come back home and check the cart (when still logged in). Stuff from #2 is not in there.

The only solution I found with the current implementation is to have the user log out and log back in, which triggers the merge of all of them.

Would there be any way to check for "new session of a logged in user" instead of login? Might not even be a solution if the user doesn't even close it's browser!?

OldStarchy commented 4 years ago

Was this fixed or just closed?

bossanova808 commented 4 years ago

Well, not exactly fixed as such - the behaviour that resulted is here - https://docs.craftcms.com/commerce/v2/adding-to-and-updating-the-cart.html#cart-merging

lukeholder commented 4 years ago

@aNickzz In Commerce 3 we removed cart merging and replaced it with: https://docs.craftcms.com/commerce/v3/adding-to-and-updating-the-cart.html#restoring-previous-cart-contents

An upgrade explanation is her:

https://docs.craftcms.com/commerce/v3/upgrading.html#cart-merging

The docs that @bossanova808 linked are for Commerce 2.1 and onwards, but the change happened in Commerce 3.

Let me know what led you here and what you are trying to accomplish and I am happy to help and hear feedback.

OldStarchy commented 4 years ago

Thanks for the heads up, at this stage I just want to retain the previous cart. The points against doing this in the doc's are good ones, but not applicable in my case. Looks like i'll have to do this in a login event.

lukeholder commented 4 years ago

@aNickzz if the user logs in they will retain their current cart before they logged in as long as something is in the cart, what is the workflow in your case?

OldStarchy commented 4 years ago

The goal is to retain items added before they logged out

  1. login (required to add items to cart)
  2. add items
  3. logout / session timeout
  4. log back in (possibly on a different device)
  5. continue with previous cart items
OldStarchy commented 4 years ago

I tested this with and without incognito (aka. "different device") and the cart was empty after logging back in.

lukeholder commented 4 years ago

@aNickzz it should work. Are you on the latest commerce release?

OldStarchy commented 4 years ago

No, currently running v3.0.5, but i'll update that soon, (currently setting up a new dev workspace)

lukeholder commented 4 years ago

Hmm, 3.0.5 should work I think. I will see if I can reproduce.

OldStarchy commented 4 years ago

This kinda works, it's just not restoring the previous cart on login, is there a setting I need to enable for this?

It does restore the previous cart when you delete the last item in your current cart though, which makes things a little confusing;

  1. login
  2. add X to cart
  3. log out
  4. login
  5. observe cart is empty
  6. add Y to cart
  7. remove Y from cart
  8. observe X is in cart

Given that I'm spending a lot of time building and testing this site, I end up with a stack of 5 - 10 carts at one time and have to go through removing all the items from each one as it gets restored after the last to fully empty the cart.

In my case, users cannot add items to the cart unless they are logged in, so I've implemented @RitterKnightCreative's code above, which effectively solves my use case.

lukeholder commented 4 years ago

@aNickzz can you confirm you are on the latest release 3.0.12?

OldStarchy commented 4 years ago

3.0.11, updates are released more frequently than I check for them.