diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f01b6569..a189992cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.10.6] - UNRELEASED +## Added +- Add lazy create cart token - @gibkigonzo (#3994) + ### Fixed - Fix low-quality images styles - @przspa (#3906) - Fix page-not-found redirect in dispatcher - @gibkigonzo (#3956) diff --git a/config/default.json b/config/default.json index 41ae29c797..18663c591b 100644 --- a/config/default.json +++ b/config/default.json @@ -239,7 +239,6 @@ "width": 150, "height": 150 }, - "bypassCartLoaderForAuthorizedUsers": true, "serverMergeByDefault": true, "serverSyncCanRemoveLocalItems": false, "serverSyncCanModifyLocalItems": false, diff --git a/core/modules/cart/store/actions.ts b/core/modules/cart/store/actions.ts index b0b63c15c7..259ba7b209 100644 --- a/core/modules/cart/store/actions.ts +++ b/core/modules/cart/store/actions.ts @@ -135,14 +135,11 @@ const actions: ActionTree = { async disconnect ({ commit }) { commit(types.CART_LOAD_CART_SERVER_TOKEN, null) }, - /** Clear the cart content + re-connect to newly created guest cart */ - async clear ({ commit, dispatch, getters }, options = { recreateAndSyncCart: true }) { + /** Clear the cart content */ + async clear ({ commit, dispatch, getters }) { await commit(types.CART_LOAD_CART, []) - if (options.recreateAndSyncCart && getters.isCartSyncEnabled) { - await commit(types.CART_LOAD_CART_SERVER_TOKEN, null) - await commit(types.CART_SET_ITEMS_HASH, null) - await dispatch('connect', { guestCart: !config.orders.directBackendSync }) // guest cart when not using directBackendSync because when the order hasn't been passed to Magento yet it will repopulate your cart - } + await commit(types.CART_LOAD_CART_SERVER_TOKEN, null) + await commit(types.CART_SET_ITEMS_HASH, null) }, /** Refresh the payment methods with the backend */ async syncPaymentMethods ({ getters, rootGetters, dispatch }, { forceServerSync = false }) { @@ -252,10 +249,8 @@ const actions: ActionTree = { Logger.info('Cart token received from cache.', 'cache', token)() Logger.info('Syncing cart with the server.', 'cart')() dispatch('sync', { forceClientState, dryRun: !config.cart.serverMergeByDefault }) - } else { - Logger.info('Creating server cart token', 'cart')() - await dispatch('connect', { guestCart: false }) } + await dispatch('create') } }, /** Get one single item from the client's cart */ @@ -350,6 +345,7 @@ const actions: ActionTree = { } productIndex++ } + await dispatch('create') if (getters.isCartSyncEnabled && getters.isCartConnected && !forceServerSilence) { return dispatch('sync', { forceClientState: true }) } else { @@ -498,16 +494,20 @@ const actions: ActionTree = { } return false }, + /** + * Create cart token when there are products in cart and we don't have token already + */ + async create ({ dispatch, getters }) { + const storedItems = getters['getCartItems'] || [] + const cartToken = getters['getCartToken'] + if (storedItems.length && !cartToken) { + Logger.info('Creating server cart token', 'cart')() + await dispatch('connect', { guestCart: false }) + } + }, /** authorize the cart after user got logged in using the current cart token */ authorize ({ dispatch }) { - Vue.prototype.$db.usersCollection.getItem('last-cart-bypass-ts', (err, lastCartBypassTs) => { - if (err) { - Logger.error(err, 'cart')() - } - if (!config.cart.bypassCartLoaderForAuthorizedUsers || (Date.now() - lastCartBypassTs) >= (1000 * 60 * 24)) { // don't refresh the shopping cart id up to 24h after last order - dispatch('connect', { guestCart: false }) - } - }) + dispatch('connect', { guestCart: false }) }, /** connect cart to the server and set the cart token */ async connect ({ getters, dispatch, commit }, { guestCart = false, forceClientState = false }) { diff --git a/core/modules/cart/test/unit/store/actions.spec.ts b/core/modules/cart/test/unit/store/actions.spec.ts index 572672cf0f..d122aaa938 100644 --- a/core/modules/cart/test/unit/store/actions.spec.ts +++ b/core/modules/cart/test/unit/store/actions.spec.ts @@ -26,7 +26,8 @@ jest.mock('@vue-storefront/core/lib/logger', () => ({ log: jest.fn(() => () => {}), debug: jest.fn(() => () => {}), warn: jest.fn(() => () => {}), - error: jest.fn(() => () => {}) + error: jest.fn(() => () => {}), + info: jest.fn(() => () => {}) } })); jest.mock('@vue-storefront/core/lib/sync', () => ({ TaskQueue: { @@ -84,39 +85,47 @@ describe('Cart actions', () => { expect(contextMock.commit).toBeCalledWith(types.CART_LOAD_CART, []); }); - it('clear dispatches creating a new cart on server with direct backend sync when its configured', async () => { - const contextMock = { - commit: jest.fn(), - dispatch: jest.fn(), - getters: { isCartSyncEnabled: true, isTotalsSyncRequired: true, isSyncRequired: true, isCartConnected: true } - }; + describe('create', () => { + it('Create cart token when there are products in cart and we don\'t have token already', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { getCartItems: [{id: 1}], getCartToken: '' } + }; - config.cart = { synchronize: true }; - config.orders = { directBackendSync: true }; + const wrapper = (actions: any) => actions.create(contextMock); + + await wrapper(cartActions); - const wrapper = (actions: any) => actions.clear(contextMock); + expect(contextMock.dispatch).toBeCalledWith('connect', {guestCart: false}); + }) + it('doesn\'t create cart token when there are NO products in cart', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { getCartItems: [], getCartToken: '' } + }; - await wrapper(cartActions); + const wrapper = (actions: any) => actions.create(contextMock); - expect(contextMock.dispatch).toBeCalledWith('connect', {guestCart: false}); - }); - - it('clear dispatches creating a new cart on server with queuing when direct backend sync is not configured', async () => { - const contextMock = { - commit: jest.fn(), - dispatch: jest.fn(), - getters: { isCartSyncEnabled: true, isTotalsSyncRequired: true, isSyncRequired: true, isCartConnected: true } - }; + await wrapper(cartActions); - config.cart = { synchronize: true }; - config.orders = { directBackendSync: false }; + expect(contextMock.dispatch).toHaveBeenCalledTimes(0); + }) + it('doesn\'t create cart token when there are products in cart but we have token already', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { getCartItems: [{id: 1}], getCartToken: 'xyz' } + }; - const wrapper = (actions: any) => actions.clear(contextMock); + const wrapper = (actions: any) => actions.create(contextMock); - await wrapper(cartActions); + await wrapper(cartActions); - expect(contextMock.dispatch).toBeCalledWith('connect', {guestCart: true}); - }); + expect(contextMock.dispatch).toHaveBeenCalledTimes(0); + }) + }) describe('sync', () => { it('doesn\'t update shipping methods if cart is empty', async () => { diff --git a/core/modules/checkout/components/Payment.ts b/core/modules/checkout/components/Payment.ts index fe7832786f..b193f2eb9f 100644 --- a/core/modules/checkout/components/Payment.ts +++ b/core/modules/checkout/components/Payment.ts @@ -1,6 +1,7 @@ import { mapState, mapGetters } from 'vuex' import RootState from '@vue-storefront/core/types/RootState' import toString from 'lodash-es/toString' +import debounce from 'lodash-es/debounce' const Countries = require('@vue-storefront/i18n/resource/countries.json') export const Payment = { @@ -69,6 +70,11 @@ export const Payment = { handler () { this.useGenerateInvoice() } + }, + paymentMethods: { + handler: debounce(function () { + this.changePaymentMethod() + }, 500) } }, methods: { diff --git a/core/modules/checkout/store/checkout/actions.ts b/core/modules/checkout/store/checkout/actions.ts index 58d1a02e72..e47b60b3fd 100644 --- a/core/modules/checkout/store/checkout/actions.ts +++ b/core/modules/checkout/store/checkout/actions.ts @@ -16,7 +16,7 @@ const actions: ActionTree = { const result = await dispatch('order/placeOrder', order, {root: true}) if (!result.resultCode || result.resultCode === 200) { Vue.prototype.$db.usersCollection.setItem('last-cart-bypass-ts', new Date().getTime()) - await dispatch('cart/clear', { recreateAndSyncCart: true }, {root: true}) + await dispatch('cart/clear', null, {root: true}) if (state.personalDetails.createAccount) { commit(types.CHECKOUT_DROP_PASSWORD) } diff --git a/core/modules/order/store/actions.ts b/core/modules/order/store/actions.ts index cce27044a8..707e35d192 100644 --- a/core/modules/order/store/actions.ts +++ b/core/modules/order/store/actions.ts @@ -32,7 +32,7 @@ const actions: ActionTree = { } Vue.prototype.$bus.$emit('order-before-placed', { order: order }) - if (!config.orders.directBackendSync || !isOnline()) { + if (!isOnline()) { commit(types.ORDER_PLACE_ORDER, order) Vue.prototype.$bus.$emit('order-after-placed', { order: order }) return { diff --git a/core/modules/user/store/actions.ts b/core/modules/user/store/actions.ts index 109dde4bd3..00f53882d6 100644 --- a/core/modules/user/store/actions.ts +++ b/core/modules/user/store/actions.ts @@ -188,7 +188,7 @@ const actions: ActionTree = { context.commit(types.USER_INFO_LOADED, res) context.dispatch('setUserGroup', res) Vue.prototype.$bus.$emit('user-after-loggedin', res) - rootStore.dispatch('cart/authorize') + context.dispatch('cart/authorize', null, { root: true }) resolve(res) resolvedFromCache = true @@ -214,7 +214,7 @@ const actions: ActionTree = { } if (!resolvedFromCache && resp.resultCode === 200) { Vue.prototype.$bus.$emit('user-after-loggedin', resp.result) - rootStore.dispatch('cart/authorize') + context.dispatch('cart/authorize', null, { root: true }) resolve(resp) } else { resolve(null) @@ -297,7 +297,7 @@ const actions: ActionTree = { context.dispatch('cart/disconnect', {}, { root: true }) .then(() => { context.dispatch('clearCurrentUser') }) .then(() => { Vue.prototype.$bus.$emit('user-after-logout') }) - .then(() => { context.dispatch('cart/clear', { recreateAndSyncCart: true }, { root: true }) }) + .then(() => { context.dispatch('cart/clear', null, { root: true }) }) if (!silent) { rootStore.dispatch('notification/spawnNotification', { type: 'success', diff --git a/docs/guide/basics/configuration.md b/docs/guide/basics/configuration.md index e157b2b6ee..eb14e639aa 100644 --- a/docs/guide/basics/configuration.md +++ b/docs/guide/basics/configuration.md @@ -256,13 +256,6 @@ Starting with Vue Storefront 1.7, we added a configuration option `config.entiti ## Cart -```json -"cart": { - "bypassCartLoaderForAuthorizedUsers": true, -``` - -The cart-loader bypass feature is there because we're posting orders to Magento asynchronously. It may happen that directly after placing an order, the Magento’s user still has the same quote ID, and after browsing through the VS store, old items will be restored to the shopping cart. Now you can disable this behavior by setting `bypassCartLoaderForAuthorizedUsers` option to `false` - ```json "cart": { "serverMergeByDefault": true, diff --git a/docs/guide/cookbook/setup.md b/docs/guide/cookbook/setup.md index 81201c4221..3e2b24b884 100644 --- a/docs/guide/cookbook/setup.md +++ b/docs/guide/cookbook/setup.md @@ -1097,7 +1097,6 @@ At [`vue-storefront/config/default.json`](https://github.com/DivanteLtd/vue-stor } }, "cart": { - "bypassCartLoaderForAuthorizedUsers": true, "serverMergeByDefault": true, "serverSyncCanRemoveLocalItems": false, "serverSyncCanModifyLocalItems": false, diff --git a/src/modules/instant-checkout/components/InstantCheckout.vue b/src/modules/instant-checkout/components/InstantCheckout.vue index 0fc41e0f82..77dad367a6 100644 --- a/src/modules/instant-checkout/components/InstantCheckout.vue +++ b/src/modules/instant-checkout/components/InstantCheckout.vue @@ -143,7 +143,7 @@ export default { this.$store.dispatch('checkout/setThankYouPage', true) this.$store.commit('ui/setMicrocart', false) this.$router.push(this.localizedRoute('/checkout')) - this.$store.dispatch('cart/clear', { recreateAndSyncCart: true }, {root: true}) + this.$store.dispatch('cart/clear', null, {root: true}) } }) }) diff --git a/src/themes/default/components/core/blocks/Microcart/Microcart.vue b/src/themes/default/components/core/blocks/Microcart/Microcart.vue index 771906f8f9..e3f0f5a874 100644 --- a/src/themes/default/components/core/blocks/Microcart/Microcart.vue +++ b/src/themes/default/components/core/blocks/Microcart/Microcart.vue @@ -204,7 +204,7 @@ export default { action1: { label: i18n.t('Cancel'), action: 'close' }, action2: { label: i18n.t('OK'), action: async () => { - await this.$store.dispatch('cart/clear', { recreateAndSyncCart: false }) // just clear the items without sync + await this.$store.dispatch('cart/clear') // just clear the items without sync await this.$store.dispatch('cart/sync', { forceClientState: true }) } },