@shopgate/engage
Version:
Shopgate's ENGAGE library.
374 lines (364 loc) • 12.3 kB
JavaScript
import { getProductsResult, productIsReady$, productsReceived$, productsReceivedCached$, RECEIVE_PRODUCTS_CACHED, variantDidChange$ } from '@shopgate/engage/product';
import { appDidStart$, routeWillEnter$, UIEvents, getCurrentRoute, hex2bin, getThemeSettings, getCurrentSearchQuery, appWillInit$, appInitialization } from '@shopgate/engage/core';
import { cartReceived$, fetchCart, cartDidEnter$, getCartItems } from '@shopgate/engage/cart';
import { userDidLogin$ } from '@shopgate/engage/user';
import { receiveFavoritesWhileVisible$ } from '@shopgate/pwa-common-commerce/favorites/streams';
import { getFavoritesProductsIds, isFetching } from '@shopgate/pwa-common-commerce/favorites/selectors';
import { categoryDidBackEnter$ } from '@shopgate/pwa-common-commerce/category/streams';
import { searchDidBackEntered$ } from '@shopgate/pwa-common-commerce/search/streams';
import { hasNewServices } from '@shopgate/engage/core/helpers';
import { cookieConsentInitialized$ } from '@shopgate/engage/tracking/streams';
import { IS_PAGE_PREVIEW_ACTIVE } from '@shopgate/engage/page/constants';
import { getUserSearch, getStoreFinderSearch, getPreferredLocation, getIsPending, getProductAlternativeLocationParams, getProductAlternativeLocations, makeGetLocation } from "./selectors";
import { fetchDefaultLocation, fetchLocations, fetchProductLocations, setPending, setUserGeolocation } from "./actions";
import { setShowInventoryInLists, showInventoryInLists } from "./helpers";
import fetchInventories from "./actions/fetchInventories";
import { EVENT_SET_OPEN } from "./providers/FulfillmentProvider";
import fetchProductInventories from "./actions/fetchProductInventories";
import { submitReservationSuccess$, userSearchChanged$, storeFinderWillEnter$, preferredLocationDidUpdateOnPDP$, provideAlternativeLocation$, preferredLocationDidUpdateGlobalOnWishlist$, storeDetailPageWillEnter$ } from "./locations.streams";
import selectLocation from "./action-creators/selectLocation";
import { NEARBY_LOCATIONS_RADIUS, SET_STORE_FINDER_SEARCH_RADIUS, NEARBY_LOCATIONS_LIMIT } from "./constants";
import selectGlobalLocation from "./action-creators/selectGlobalLocation";
let initialLocationsResolve;
let initialLocationsReject;
const initialLocationsPromise = new Promise((resolve, reject) => {
initialLocationsResolve = resolve;
initialLocationsReject = reject;
});
/**
* Sets a location once the location has been validated.
* @param {string} locationCode Location code
* @param {Function} dispatch Redux dispatch function
* @returns {Promise}
*/
const setLocationOnceAvailable = async (locationCode, dispatch) => {
try {
const {
locations: initialLocations
} = await initialLocationsPromise;
if (!initialLocations.some(l => l.code === locationCode)) {
return;
}
await dispatch(selectLocation({
location: {
code: locationCode
}
}));
requestAnimationFrame(() => {
dispatch(setPending(false));
});
} catch (error) {
// Location won't be set.
}
};
/**
* Locations subscriptions.
* @param {Function} subscribe The subscribe function.
*/
function locationsSubscriber(subscribe) {
subscribe(appWillInit$, () => {
appInitialization.set('location', async ({
dispatch
}) => {
// Skip fetching the default location if the page preview is active, since it blocks
// iFrame communication.
if (hasNewServices() && !IS_PAGE_PREVIEW_ACTIVE) {
await dispatch(fetchDefaultLocation());
}
});
});
subscribe(cookieConsentInitialized$, async ({
dispatch,
getState
}) => {
if (!hasNewServices()) {
// no ROPE stuff when connected with old services right now
return;
}
// Fetch merchants locations.
const userSearch = getUserSearch(getState());
try {
const {
locations
} = await dispatch(fetchLocations(userSearch));
const preferredLocation = getPreferredLocation(getState());
if (preferredLocation) {
const {
code
} = preferredLocation;
// Check if the preferred location is included within the fetched locations
const hasLocation = !!locations.find(location => location.code === code);
if (!hasLocation) {
// Fetch the missing location data
await dispatch(fetchLocations({
codes: [code]
}));
}
}
// Preset preferredLocation if configured
const {
preferredLocationDefault
} = getThemeSettings('@shopgate/engage/locations') || {};
if (preferredLocationDefault) {
// check if there is already a preferredLocation for the user, if not set one
if (!preferredLocation) {
const locationToPreselect = locations.find(l => l.code === preferredLocationDefault);
if (locationToPreselect) {
await dispatch(selectLocation({
location: {
code: preferredLocationDefault
}
}));
}
}
}
initialLocationsResolve(locations);
} catch (error) {
initialLocationsReject(error);
}
UIEvents.addListener(EVENT_SET_OPEN, () => {
const route = getCurrentRoute(getState());
if (!route.params.productId && !route.state.productId) {
return;
}
const productId = route.state.productId || hex2bin(route.params.productId);
if (productId) {
dispatch(fetchProductLocations(productId, getUserSearch(getState())));
}
});
});
subscribe(userSearchChanged$, async ({
dispatch,
getState,
action
}) => {
const {
productId,
isStoreFinder,
silent
} = action;
if (silent === true) {
// Silent background propagation
return;
}
const state = getState();
const userSearch = getUserSearch(state);
if (isStoreFinder || action.type === SET_STORE_FINDER_SEARCH_RADIUS) {
const storeFinderSearch = getStoreFinderSearch(state);
await dispatch(fetchLocations({
...userSearch,
...storeFinderSearch,
enableInLocationFinder: true
}));
} else if (!productId) {
await dispatch(fetchLocations(userSearch));
} else {
await dispatch(fetchProductLocations(productId, userSearch));
}
});
const productInventoryNeedsUpdate$ = productIsReady$.merge(variantDidChange$).merge(preferredLocationDidUpdateOnPDP$).debounceTime(200);
subscribe(productInventoryNeedsUpdate$, ({
action,
dispatch,
getState
}) => {
const {
productData
} = action;
// Skip if no fulfillment methods are set.
if (!productData || !productData.fulfillmentMethods || productData.fulfillmentMethods.length === 0) {
return;
}
const state = getState();
const preferredLocation = getPreferredLocation(state);
if (!preferredLocation) {
return;
}
// Fetch inventories for this specific product.
dispatch(fetchProductInventories(action.productData.id, {
locationCodes: [preferredLocation.code]
}));
});
// Core config and cart subscriptions
const fetchCart$ = cartDidEnter$.switchMap(() => submitReservationSuccess$.first()).delay(500);
subscribe(fetchCart$, ({
dispatch
}) => {
dispatch(fetchCart());
});
subscribe(storeFinderWillEnter$, async ({
dispatch,
getState
}) => {
const state = getState();
// Fetch merchants locations.
const userSearch = getUserSearch(state);
const storeFinderSearch = getStoreFinderSearch(state);
await dispatch(fetchLocations({
...userSearch,
...storeFinderSearch,
enableInLocationFinder: true
}));
});
/**
* Makes sure that the active location is switched after logging in
* to a location that is also available in the cart.
* Avoids having a selected location that differs from the cart
*/
const afterCartMerge$ = userDidLogin$.mergeMap(() => cartReceived$.first());
subscribe(afterCartMerge$, async ({
dispatch,
getState
}) => {
const state = getState();
const cartItems = getCartItems(state);
const preferredLocation = getPreferredLocation(state, {});
if (!cartItems?.length) {
return;
}
const activeCartLocation = cartItems.find(item => item.fulfillment?.location?.code === preferredLocation?.code);
if (activeCartLocation) {
return;
}
const firstLocationCode = cartItems[0]?.fulfillment?.location?.code;
if (!firstLocationCode) {
return;
}
await dispatch(selectLocation({
location: {
code: firstLocationCode
}
}));
dispatch(selectGlobalLocation({
code: firstLocationCode
}));
});
/**
* Handles an added store url parameter that will set the default store location
*/
subscribe(routeWillEnter$, ({
action,
dispatch,
getState
}) => {
const locationCode = action.route.query.store;
if (!locationCode) {
if (!getIsPending(getState())) {
dispatch(setPending(false));
}
return;
}
setLocationOnceAvailable(locationCode, dispatch);
});
const alternative$ = productInventoryNeedsUpdate$.switchMap(() => provideAlternativeLocation$.first());
/**
* Provide alternative location on PDP when preferred location is out of stock
*/
subscribe(alternative$, async ({
action,
dispatch,
getState
}) => {
// Refresh geo location
await dispatch(setUserGeolocation({
silent: true
}));
// Get new state with geolocation
const state = getState();
const alternativeLocations = getProductAlternativeLocations(state, action);
if (alternativeLocations) {
// Already fetched by default params
return;
}
const {
productId,
params
} = action;
const alternativeParams = getProductAlternativeLocationParams(state);
const fetchParams = {
...alternativeParams,
...params
};
if (fetchParams.geolocation || fetchParams.postalCode) {
dispatch(fetchProductLocations(productId, fetchParams));
}
});
subscribe(categoryDidBackEnter$.merge(searchDidBackEntered$), ({
action,
dispatch,
getState
}) => {
const state = getState();
if (!showInventoryInLists(state)) {
return;
}
const {
categoryId
} = action.route.params;
const query = getCurrentSearchQuery(state);
const products = getProductsResult(state, {
categoryId: hex2bin(categoryId),
searchPhrase: query
})?.products;
if (!products || !products.length) {
return;
}
const productCodes = products.map(({
id
}) => id);
dispatch(fetchInventories(productCodes));
});
subscribe(productsReceived$.merge(productsReceivedCached$), ({
action,
dispatch,
getState
}) => {
if (!showInventoryInLists(getState())) {
return;
}
if (!action.products || !action.products.length || action?.fetchInventory === false) {
return;
}
const productCodes = action.type !== RECEIVE_PRODUCTS_CACHED ? action.products.map(({
id
}) => id) : action.products;
dispatch(fetchInventories(productCodes));
});
subscribe(receiveFavoritesWhileVisible$.merge(preferredLocationDidUpdateGlobalOnWishlist$), ({
dispatch,
getState
}) => {
const state = getState();
if (!showInventoryInLists(state) || isFetching(getState())) {
return;
}
const productIds = getFavoritesProductsIds(state);
if (!productIds || !productIds.length) {
return;
}
dispatch(fetchInventories(productIds));
});
subscribe(appDidStart$, ({
getState
}) => {
// enable inventory in product lists for some users
setShowInventoryInLists(getState());
});
subscribe(storeDetailPageWillEnter$, async ({
dispatch,
getState
}) => {
const route = getCurrentRoute(getState());
const getLocation = makeGetLocation(() => route.params.code);
const location = getLocation(getState());
await dispatch(fetchLocations({
geolocation: {
longitude: location.longitude,
latitude: location.latitude
},
limit: NEARBY_LOCATIONS_LIMIT,
radius: NEARBY_LOCATIONS_RADIUS
}));
});
}
export default locationsSubscriber;