Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@
"width": 150,
"height": 150
},
"bypassCartLoaderForAuthorizedUsers": true,
"serverMergeByDefault": true,
"serverSyncCanRemoveLocalItems": false,
"serverSyncCanModifyLocalItems": false,
Expand Down
36 changes: 18 additions & 18 deletions core/modules/cart/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,11 @@ const actions: ActionTree<CartState, RootState> = {
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 }) {
Expand Down Expand Up @@ -252,10 +249,8 @@ const actions: ActionTree<CartState, RootState> = {
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 */
Expand Down Expand Up @@ -350,6 +345,7 @@ const actions: ActionTree<CartState, RootState> = {
}
productIndex++
}
await dispatch('create')
if (getters.isCartSyncEnabled && getters.isCartConnected && !forceServerSilence) {
return dispatch('sync', { forceClientState: true })
} else {
Expand Down Expand Up @@ -498,16 +494,20 @@ const actions: ActionTree<CartState, RootState> = {
}
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 }) {
Expand Down
61 changes: 35 additions & 26 deletions core/modules/cart/test/unit/store/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 () => {
Expand Down
6 changes: 6 additions & 0 deletions core/modules/checkout/components/Payment.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -69,6 +70,11 @@ export const Payment = {
handler () {
this.useGenerateInvoice()
}
},
paymentMethods: {
handler: debounce(function () {
this.changePaymentMethod()
}, 500)
}
},
methods: {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/checkout/store/checkout/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const actions: ActionTree<CheckoutState, RootState> = {
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)
}
Expand Down
2 changes: 1 addition & 1 deletion core/modules/order/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const actions: ActionTree<OrderState, RootState> = {
}

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 {
Expand Down
6 changes: 3 additions & 3 deletions core/modules/user/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const actions: ActionTree<UserState, RootState> = {
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
Expand All @@ -214,7 +214,7 @@ const actions: ActionTree<UserState, RootState> = {
}
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)
Expand Down Expand Up @@ -297,7 +297,7 @@ const actions: ActionTree<UserState, RootState> = {
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',
Expand Down
7 changes: 0 additions & 7 deletions docs/guide/basics/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion docs/guide/cookbook/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
},
Expand Down