@shopgate/pwa-common-commerce
Version:
Commerce library for the Shopgate Connect PWA.
266 lines (252 loc) • 11 kB
JavaScript
import pipelineDependencies from '@shopgate/pwa-core/classes/PipelineDependencies';
import appConfig from '@shopgate/pwa-common/helpers/config';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';
import { appDidStart$ } from '@shopgate/pwa-common/streams';
import groupBy from 'lodash/groupBy';
import ToastProvider from '@shopgate/pwa-common/providers/toast';
import { makeGetRoutePattern } from '@shopgate/engage/core/selectors';
import { getLoadWishlistOnAppStartEnabled, getWishlistItemQuantityEnabled } from '@shopgate/engage/core/selectors/shopSettings';
import { LoadingProvider } from '@shopgate/pwa-common/providers';
import { favoritesWillEnter$, shouldFetchFreshFavorites$, addProductToFavoritesDebounced$, addProductToFavorites$, removeProductFromFavoritesDebounced$, errorFavoritesLimit$, refreshFavorites$, didReceiveFlushFavoritesBuffer$, updateProductInFavoritesDebounced$, favoritesDidAddItem$, favoritesSyncIdle$ } from "../streams";
import { SHOPGATE_USER_ADD_FAVORITES, SHOPGATE_USER_DELETE_FAVORITES } from "../constants/Pipelines";
import addFavorites from "../actions/addFavorites";
import updateFavorites from "../actions/updateFavorites";
import removeFavorites from "../actions/removeFavorites";
import fetchFavoritesListsWithItems from "../actions/fetchFavoritesListsWithItems";
import fetchFavorites from "../actions/fetchFavorites";
import { requestAddFavorites, requestRemoveFavorites, cancelRequestSyncFavorites, errorFavorites, idleSyncFavorites, requestUpdateFavorites } from "../action-creators";
import { REQUEST_ADD_FAVORITES, REQUEST_REMOVE_FAVORITES, FAVORITES_LIMIT_ERROR, REQUEST_UPDATE_FAVORITES, FAVORITES_PATH } from "../constants";
import { getFavoritesCount, makeGetFavoritesCountByList, makeGetProductRelativesOnFavorites, getFavoritesProducts, getUseGetFavoriteIdsPipeline } from "../selectors";
import fetchFavoriteIds from "../actions/fetchFavoriteIds";
/**
* @param {Function} subscribe Subscribes to an observable.
*/
export default function favorites(subscribe) {
if (!appConfig.hasFavorites) {
return;
}
/** App start */
subscribe(appDidStart$, async ({
dispatch,
getState
}) => {
// Setup sync pipeline dependencies (concurrency to each other and themselves)
pipelineDependencies.set(SHOPGATE_USER_ADD_FAVORITES, [SHOPGATE_USER_ADD_FAVORITES, SHOPGATE_USER_DELETE_FAVORITES]);
pipelineDependencies.set(SHOPGATE_USER_DELETE_FAVORITES, [SHOPGATE_USER_ADD_FAVORITES, SHOPGATE_USER_DELETE_FAVORITES]);
const loadWishlistOnAppStartEnabled = getLoadWishlistOnAppStartEnabled(getState());
if (loadWishlistOnAppStartEnabled && makeGetRoutePattern()(getState()) !== FAVORITES_PATH) {
await dispatch(fetchFavoritesListsWithItems(false));
}
});
/** Favorites route enter */
subscribe(favoritesWillEnter$, async ({
dispatch
}) => {
await dispatch(fetchFavoritesListsWithItems(true));
});
/** User login / logout */
subscribe(shouldFetchFreshFavorites$, async ({
dispatch
}) => {
await dispatch(fetchFavoritesListsWithItems(true));
});
subscribe(addProductToFavoritesDebounced$, ({
action,
dispatch,
getState
}) => {
const state = getState();
const wishlistItemQuantityEnabled = getWishlistItemQuantityEnabled(state);
// Nothing to do, when the store already contains the item
const activeProductInList = getFavoritesProducts(state).byList[action.listId]?.items.find(({
productId
}) => productId === action.productId);
if (activeProductInList && !wishlistItemQuantityEnabled) {
// Call cancel action with "zero" count, because request was even dispatched
dispatch(cancelRequestSyncFavorites(0, action.listId));
return;
}
const {
favorites: {
limit = 100
} = {}
} = appConfig;
const count = makeGetFavoritesCountByList(() => action?.listId)(state);
// When the getFavorites pipeline is used to fetch favorites, the amount of items is limited
// since it returns full product entities. Hence adding more items needs to be prevented.
if (limit && count >= limit && !getUseGetFavoriteIdsPipeline(state)) {
// Dispatch a local error only, because the request to add is prevented
const error = new Error('Limit exceeded');
error.code = FAVORITES_LIMIT_ERROR;
dispatch(errorFavorites(action.productId, error));
} else {
dispatch(requestAddFavorites(action.productId, action.listId, action.quantity, action.notes, action.showToast));
}
});
subscribe(updateProductInFavoritesDebounced$, ({
action,
dispatch
}) => {
dispatch(requestUpdateFavorites(action.productId, action.listId, action.quantity, action.notes));
});
subscribe(removeProductFromFavoritesDebounced$, ({
action,
dispatch,
getState
}) => {
const count = getFavoritesCount(getState());
if (count > 0) {
if (action.withRelatives) {
// Will only handle ids which are present in the store, no additional check needed
const allOnList = makeGetProductRelativesOnFavorites(() => action.listId)(getState(), {
productId: action.productId
});
allOnList.forEach(id => dispatch(requestRemoveFavorites(id, action.listId)));
return;
}
// Avoids trying to remove something that was already removed (incoming fetch response)
const list = getFavoritesProducts(getState()).byList[action.listId];
if (!list?.items.find(({
productId
}) => productId === action.productId)) {
// Call cancel action with "zero" count, because request was even dispatched
dispatch(cancelRequestSyncFavorites(0, action.listId));
return;
}
dispatch(requestRemoveFavorites(action.productId, action.listId));
} else if (!getFavoritesProducts(getState()).byList[action.listId]?.isFetching) {
// Remove should not be possible when no favorites available
// Refresh to fix inconsistencies, by dispatching an idleSync action when not fetching
dispatch(idleSyncFavorites(action.listId));
}
});
// Catch local limit errors (backend errors are handled autonomously)
subscribe(errorFavoritesLimit$, ({
dispatch
}) => {
dispatch(showModal({
confirm: null,
dismiss: 'modal.ok',
title: 'modal.title_error',
message: 'favorites.error_limit'
}));
});
/**
* Request after N seconds since last add or remove request to make sure
* backend did actually save it
*/
subscribe(refreshFavorites$, async ({
dispatch,
action,
getState
}) => {
if (action.listId) {
const useGetFavoriteIdsPipeline = getUseGetFavoriteIdsPipeline(getState());
if (useGetFavoriteIdsPipeline) {
dispatch(fetchFavoriteIds(true, action.listId));
} else {
dispatch(fetchFavorites(true, action.listId));
}
return;
}
await dispatch(fetchFavoritesListsWithItems(true));
});
/**
* Takes an action buffer of the request-add, request-remove and request-update favorites actions
* and triggers Pipeline requests for all of them. Errors are handled autonomously.
* After all pipeline requests are done it resets the favorite page's state to "idle".
*/
subscribe(didReceiveFlushFavoritesBuffer$, async actionBuffer => {
// This subscription handles bulk actions only
if (!Array.isArray(actionBuffer) || !actionBuffer.length) {
return;
}
LoadingProvider.setLoading(FAVORITES_PATH);
// All actions provide the same functionality, just take the first entry
const {
dispatch
} = actionBuffer[0];
// Group all buffered actions by listId and productID
const actions = actionBuffer.map(({
action
}) => action);
const actionsByListAndProduct = groupBy(actions, ({
listId,
productId
}) => `${listId}-${productId}}`);
const idleLists = [];
await Object.values(actionsByListAndProduct).forEach(async groupedActions => {
const [{
productId,
listId,
quantity,
notes,
showToast
} = {}] = groupedActions;
const updateActions = groupedActions.filter(action => action.type === REQUEST_UPDATE_FAVORITES);
const addActions = groupedActions.filter(action => action.type === REQUEST_ADD_FAVORITES);
const removeActions = groupedActions.filter(action => action.type === REQUEST_REMOVE_FAVORITES);
try {
// If there are any update actions we only dispatch the last one
if (updateActions.length > 0) {
const [lastUpdateAction] = updateActions.slice(-1);
await dispatch(updateFavorites(lastUpdateAction.productId, lastUpdateAction.listId, lastUpdateAction.quantity, lastUpdateAction.notes));
}
// Sum up all adds and removes, based on sum dispatch add / remove
const addRemoveBalance = addActions.length - removeActions.length;
if (addRemoveBalance > 0) {
await dispatch(addFavorites(productId, listId, quantity, notes, showToast));
}
if (addRemoveBalance < 0) {
await dispatch(removeFavorites(productId, listId, quantity, notes));
}
if (updateActions.length === 0 && addRemoveBalance === 0) {
dispatch(cancelRequestSyncFavorites(groupedActions.length, listId));
} else if (!idleLists.includes(listId)) {
idleLists.push(listId);
await dispatch(idleSyncFavorites(listId));
}
} catch (error) {
if (!idleLists.includes(listId)) {
idleLists.push(listId);
await dispatch(idleSyncFavorites(listId));
}
}
});
});
subscribe(favoritesSyncIdle$, () => {
LoadingProvider.resetLoading(FAVORITES_PATH);
});
subscribe(addProductToFavorites$, ({
events,
getState,
action
}) => {
const loadWishlistOnAppStartEnabled = getLoadWishlistOnAppStartEnabled(getState());
// When wish list loading on app start is disabled, toast is shown instantly after the add
// action was dispatched. We don't have to wait for the "debounced" action, since
// removal of wishlist items is not possible.
if (!loadWishlistOnAppStartEnabled && action?.showToast !== false) {
events.emit(ToastProvider.ADD, {
id: 'favorites.added',
message: 'favorites.added'
});
}
});
subscribe(favoritesDidAddItem$, ({
events,
getState,
action
}) => {
const loadWishlistOnAppStartEnabled = getLoadWishlistOnAppStartEnabled(getState());
// When wish list loading on app start is enabled, toast is shown after the add action from
// the buffer system is dispatched. We wait for that since the item might have been removed
// again within the debounce time.
if (loadWishlistOnAppStartEnabled && action?.showToast !== false) {
events.emit(ToastProvider.ADD, {
id: 'favorites.added',
message: 'favorites.added'
});
}
});
}