UNPKG

@shopgate/pwa-common-commerce

Version:

Commerce library for the Shopgate Connect PWA.

759 lines (707 loc) • 24.6 kB
import "core-js/modules/es.regexp.flags.js"; import { createSelector } from 'reselect'; import isEqual from 'lodash/isEqual'; import { getCurrentState } from '@shopgate/pwa-common/selectors/router'; import { logger } from '@shopgate/pwa-core/helpers'; import { generateResultHash } from '@shopgate/pwa-common/helpers/redux'; import { getSortOrder } from '@shopgate/pwa-common/selectors/history'; import { SORT_SCOPE_CATEGORY, SORT_SCOPE_SEARCH } from '@shopgate/engage/filter/constants'; import { getPreferredLocation } from '@shopgate/engage/locations/selectors'; import { getIsLocationBasedShopping } from '@shopgate/engage/core/selectors/merchantSettings'; import { getActiveFilters } from "../../filter/selectors"; import { filterProperties } from "../helpers"; /** * Shared empty product state to keep the fallback referentially stable. */ const EMPTY_PRODUCT_STATE = {}; /** * Retrieves the product state from the store. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @return {Object} The product state. */ export const getProductState = state => state.product || EMPTY_PRODUCT_STATE; /** * Selects all products from the store. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @return {Object} The collection of products. */ export const getProducts = createSelector(getProductState, state => state.productsById || {}); /** * Selects the product shipping state. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The product shipping state. */ export const getProductShippingState = createSelector(getProductState, state => state.shippingByProductId || {}); /** * Selects the product description state. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The product description state. */ export const getProductDescriptionState = createSelector(getProductState, state => state.descriptionsByProductId || {}); /** * Selects the product properties state. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The product properties state. */ export const getProductPropertiesState = createSelector(getProductState, state => state.propertiesByProductId || {}); /** * Selects the product images state. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The product images state. */ export const getProductImagesState = createSelector(getProductState, state => state.imagesByProductId || {}); /** * Selects the product variants state. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The product variants state. */ export const getProductVariantsState = createSelector(getProductState, state => state.variantsByProductId || {}); /** * Retrieves a product by id from state. Different to getProduct() which returns the product * entity data if available, this selector returns the pure state entry for a given productId. * So the expires and the isFetching property is processable. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} The dedicated product. */ export const getProductById = createSelector(getProducts, (state, props) => props, (products, props) => { if (typeof props !== 'object') { logger.warn('Invocation of getProductById() with a productId will be deprecated soon. Please provide a props object.'); return products[props] || null; } if (!props.productId) { return null; } return products[props.productId] || null; }); /** * @deprecated Also exists within @shopgate/engage/product/selectors/product */ export const getProductDataById = createSelector(getProductById, product => product ? product.productData : undefined); /** * Retrieves the id of the current selected product from the component props. When the props * contain a variant id it will return this one instead of the product id. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @param {Object} [props] The component props. * @return {string|null} The id of the current product. */ export const getProductId = (state, props) => { if (!state) { return null; } if (typeof props === 'undefined') { /** * Before PWA 6.0 some product selectors relied on a "currentProduct" state which doesn't exist * anymore. Their successors require a props object which contains a productId or a variantId. * To support debugging an error will be logged, if the props are missing at invocation. */ logger.error('getProductId() needs to be called with a props object that includes a productId.'); return null; } // Since a variantId can have falsy values, we need an "undefined" check here. if (typeof props.variantId !== 'undefined' && props.variantId !== null) { return props.variantId; } return props.productId || null; }; /** * Gets the variant id out of the selector props. * @param {Object} state The current application state. * @param {Object} props The component props. * @returns {string|null} */ export const getVariantProductId = (state, props) => { if (typeof props === 'undefined') { /** * Before PWA 6.0 the variant selectors relied on a "currentProduct" state which doesn't exist * anymore. Their successors require a props object which contains a variantId. * To support debugging an error will be logged, if the props are missing at invocation. */ logger.error('getVariantId() needs to be called with a props object that includes a variantId.'); } const { variantId = null } = props || {}; return variantId; }; /** * Checks if currently a variant is selected within the props. * @param {Object} state The current application state. * @param {Object} props The component props. * @returns {boolean} */ export const isVariantSelected = (state, props) => !!getVariantProductId(state, props); /** * Retrieves the product data for the passed productId from the store. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object} The current product. */ export const getProduct = createSelector(getProducts, getProductId, (products, productId) => { const { productData } = products[productId] || {}; return productData || null; }); /** * Retrieves the product name. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getProductName = createSelector(getCurrentState, getProduct, (routeState, product) => { if (!product) { if (!routeState || !routeState.title) { return null; } return routeState.title; } return product.name; }); /** * Retrieves the product long name. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getProductLongName = createSelector(getCurrentState, getProduct, (routeState, product) => { if (!product) { if (!routeState || !routeState.title) { return null; } return routeState.title; } if (!product.longName) { return product.name; } return product.longName; }); /** * Retrieves the product rating. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductRating = createSelector(getProduct, product => { if (!product) { return null; } return product.rating; }); /** * Retrieves the product manufacturer. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getProductManufacturer = createSelector(getProduct, product => { if (!product) { return null; } return product.manufacturer; }); /** * Retrieves the product stock information. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductStock = createSelector(getProduct, product => { if (!product) { return null; } return product.stock; }); /** * Retrieves the product availability. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductAvailability = createSelector(getProduct, product => { if (!product) { return null; } return product.availability; }); /** * Retrieves the product flags. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductFlags = createSelector(getProduct, product => { if (!product) { return null; } return product.flags; }); /** * Retrieves the product price object. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string} */ export const getProductPriceData = createSelector(getProduct, product => { if (!product) { return null; } return product.price; }); /** * Retrieves the product currency. * @param {Object} state The current application state. * @param {Object} props The component props. * @todo Move to the price selectors * @return {string|null} */ export const getProductCurrency = createSelector(getProductPriceData, price => { if (!price) { return null; } return price.currency; }); /** * Retrieves the product discount. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getProductDiscount = createSelector(getProductPriceData, price => { if (!price) { return null; } return price.discount || 0; }); /** * Retrieves the unit price from a product. * @param {Object} state The current application state. * @param {Object} props The component props. * @todo Move to the price selectors * @return {number|null} */ export const getProductUnitPrice = createSelector(getProductPriceData, price => { if (!price) { return null; } return price.unitPrice; }); /** * Determines if a product has variants. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {boolean} */ export const hasProductVariants = createSelector(getProductFlags, flags => { if (!flags) { return null; } return flags.hasVariants; }); /** * Determines if a product has variety (variants, options). * This product can not be added to a cart. Selecting of variety should be done on PDP * @param {Object} state The current application state. * @param {Object} props The component props. * @return {boolean} */ export const hasProductVariety = createSelector(getProductFlags, flags => { if (!flags) { return null; } return flags.hasVariants || flags.hasOptions; }); /** * Determines if a product is a base product. * @param {Object} state The current application state. * @param {Object} props The component props. * @todo Check if returning null is correct. * @return {boolean|null} */ export const isBaseProduct = createSelector(getProduct, hasProductVariants, (product, hasVariants) => { if (!product) { return false; } /** * Base products are simple products without variants or products with related variant products. * At variant products the baseProductId is used to reference the base product. */ return product.baseProductId === null || hasVariants; }); /** * Determines a baseProductId for the products which are referenced within the props. * When a variantId is passed, the selector will return the id of the related base product. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getBaseProductId = createSelector(getProduct, (_, props = {}) => props.productId, (_, props = {}) => props.variantId, (product, productId, variantId) => { if (!product) { // Return the productId when both ids are present, but no variant product is available yet. if (typeof productId !== 'undefined' && typeof variantId !== 'undefined') { return productId; } return null; } // First try to determine a baseProductId for a selected product const { baseProductId = null } = product; return baseProductId || product.id; }); /** * Retrieves the base product data for the passed productId from the store. * @deprecated Also exists within @shopgate/engage/product/selectors/product * @param {Object} state The current application state. * @returns {Object|null} The current product. */ export const getBaseProduct = createSelector(getProducts, getBaseProductId, (products, baseProductId) => { if (!baseProductId) { return null; } const { productData = null } = products[baseProductId] || {}; return productData; }); /** * Determines if a base product has variants. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {boolean} */ export const hasBaseProductVariants = createSelector(getBaseProduct, baseProduct => { if (!baseProduct) { return false; } const { flags: { hasVariants = false } = {} } = baseProduct; return hasVariants; }); /** * Retrieves the metadata for the given product. * @param {Object} state The current application state. * @return {Object|null} */ export const getProductMetadata = createSelector(getProduct, product => { if (!product) { return null; } return product.metadata; }); /** * Retrieves the metadata for the given product. * @param {Object} state The current application state. * @return {Object|null} */ export const getBaseProductMetadata = createSelector(getBaseProduct, product => { if (!product) { return null; } return product.metadata; }); /** * Retrieves the shipping data for the given product. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductShipping = createSelector(getProductShippingState, getProductId, (shipping, productId) => { const entry = shipping[productId]; if (!entry || !entry.shipping) { return null; } return entry.shipping; }); export const getProductPropertiesUnfiltered = createSelector(getProductId, getProductPropertiesState, (productId, properties) => { const entry = properties[productId]; if (!entry || entry.isFetching || typeof entry.properties === 'undefined') { return null; } return entry.properties; }); /** * Retrieves the properties for the given product. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductProperties = createSelector(getProductPropertiesState, getProductId, (properties, productId) => { const entry = properties[productId]; if (!entry || !entry.properties) { return null; } return filterProperties(entry.properties); }); /** * Retrieves the description for the given product. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getProductDescription = createSelector(getProductDescriptionState, getProductId, (descriptions, productId) => { const entry = descriptions[productId]; if (!entry || typeof entry.description === 'undefined') { return null; } return entry.description; }); /** * Retrieves the images for the given product. If the props contain a variantId, and the related * product does not have images, the selector tries to pick images from its base product. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Array|null} */ export const getProductImages = createSelector(getProductImagesState, getProductId, getBaseProductId, (images, productId, baseProductId) => { const { images: productImages, isFetching } = images[productId] || {}; if (isFetching) { return null; } // If the product doesn't have images after fetching if (baseProductId && (!Array.isArray(productImages) || !productImages.length)) { // ...check the base product. const { images: baseProductImages } = images[baseProductId] || {}; if (!Array.isArray(baseProductImages) || !baseProductImages.length) { return null; } return baseProductImages; } return productImages || null; }); export const getFeaturedImage = createSelector(getProduct, getBaseProduct, (product, baseProduct) => { let productImage = null; let baseProductImage = null; if (product?.featuredMedia) { productImage = product.featuredMedia.type === 'image' ? product.featuredMedia.url : null; } if (baseProduct?.featuredMedia) { baseProductImage = baseProduct.featuredMedia.type === 'image' ? baseProduct.featuredMedia.url : null; } return productImage || baseProductImage || product?.featuredImageBaseUrl || product?.featuredImageUrl; }); /** * Retrieves the product variant data. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getProductVariants = createSelector(getProductVariantsState, getBaseProductId, (variants, baseProductId) => { const entry = variants[baseProductId]; if (!entry || !entry.variants) { return null; } return entry.variants; }); /** * Retrieves a product for the selected variant id from the store. * @param {Object} state The current application state. * @param {Object} props The component props. * @returns {Object|null} The selected variant or null if none is selected */ export const getSelectedVariant = createSelector(getProduct, isVariantSelected, (product, selected) => { if (!product || !selected) { return null; } return product; }); /** * Determines if a product is orderable. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {boolean} */ export const isProductOrderable = createSelector(getProductStock, stockInfo => !!(stockInfo && stockInfo.orderable)); /** * Retrieves the product id of a variant product. When no variantId is passed within * the props, the selector will return NULL. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {string|null} */ export const getVariantId = createSelector(getProduct, product => { if (!product) { return null; } const { id, baseProductId } = product; return baseProductId !== null ? id : null; }); /** * Retrieves an availability object for a passed set of variant characteristics. * @param {Object} state The current application state. * @param {Object} props The component props. * @return {Object|null} */ export const getVariantAvailabilityByCharacteristics = createSelector(getProductVariants, (state, props = {}) => props.characteristics, (variants, characteristics) => { if (!variants) { return null; } const found = variants.products.find(product => isEqual(product.characteristics, characteristics)); if (!found) { return null; } return found.availability; }); /** * Creates a selector that determines fulfillment params for product requests */ export const getFulfillmentParams = createSelector(getIsLocationBasedShopping, getPreferredLocation, (isLocationBasedShopping, location) => { if (!isLocationBasedShopping || !location) { return {}; } return { locationCodes: [location.code] }; }); /** * Retrieves the generated result hash for a category id or search phrase. * @param {Object} state The current application state. * @param {Object} props The component props. * @returns {string|null} The result hash. */ export const getResultHash = createSelector((state, props = {}) => props.categoryId, (state, props = {}) => props.searchPhrase, (state, props = {}) => props.params, (state, props) => getSortOrder(state, props), getActiveFilters, getFulfillmentParams, (categoryId, searchPhrase, params, sort, filters, fulfillmentParams) => { if (categoryId) { return generateResultHash({ categoryId, sort, ...(filters && { filters }), ...params, ...fulfillmentParams }); } if (searchPhrase) { return generateResultHash({ searchPhrase, sort, ...params, ...(filters && { filters }), ...fulfillmentParams }); } return null; }); /** * Retrieves the result by hash. * @param {Object} state The application state. * @param {Object} props The component props. * @returns {Object} The result. */ export const getResultByHash = createSelector(getProductState, getResultHash, (productState, hash) => { const results = productState.resultsByHash[hash]; if (!results) { return null; } return results; }); /** * Populates the product result object. * @param {Object} state The application state. * @param {Object} props The component props. * @param {string} hash The result hash. * @param {Object} result The result. * @return {Object} The product result. */ export const getPopulatedProductsResult = (state, props, hash, result) => { const { searchPhrase } = props; const sort = getSortOrder(state, { ...props, scope: typeof searchPhrase === 'undefined' ? SORT_SCOPE_CATEGORY : SORT_SCOPE_SEARCH }); let products = []; let totalProductCount = !hash ? 0 : null; const expired = !!(result && result.expires && result.expires > 0 && result.expires < Date.now()); if (result && result.products) { totalProductCount = result.totalResultCount; products = result.products.map(productId => getProductById(state, { productId }).productData); } return { products, totalProductCount, sort, hash, expired }; }; /** * Retrieves the populated product result. * @param {Object} state The application state. * @param {Object} props The component props. * @returns {Object} The product result. */ export const getProductsResult = createSelector(state => state, (state, props) => props, getResultHash, getResultByHash, getPopulatedProductsResult); /** * Selector factory which creates a selector to retrieve a product results object based on a custom * hash string. * @param {string} hash A resultsByHash hash string * @returns {Function} */ export const makeGetProductResultByCustomHash = hash => { const parsedHash = JSON.parse(hash); return createSelector(state => state, (state, props) => props, getProductState, (state, props, productState) => { const { searchPhrase } = props; let products = []; let totalProductCount = !hash ? 0 : null; const sort = parsedHash?.sort || getSortOrder(state, { ...props, scope: typeof searchPhrase === 'undefined' ? SORT_SCOPE_CATEGORY : SORT_SCOPE_SEARCH }); const result = productState.resultsByHash[hash]; if (result && result.products) { totalProductCount = result.totalResultCount; products = result.products.map(productId => getProductById(state, { productId }).productData); } return { products, totalProductCount, sort, hash }; }); }; /** * Selector mappings for PWA < 6.0 * @deprecated */ export const getCurrentProduct = getProduct; export const getCurrentProductId = getProductId; export const getCurrentBaseProductId = getBaseProductId; export const getCurrentBaseProduct = getBaseProduct; export const getCurrentProductStock = getProductStock; export const getProductStockInfo = getProductStock; export const getProductBasePrice = getProductUnitPrice; export const isOrderable = isProductOrderable;