Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support product effectivity dates on Product Detail Page #668

Merged
merged 9 commits into from
May 23, 2019
9 changes: 9 additions & 0 deletions extensions/@shopgate-theme-config/extension-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
],
"input": [{"key": "products"}],
"output": [{"key": "products"}]
},
{
"path": "extension/mockEffectiveDates.js",
"hooks": [
"shopgate.catalog.getProducts.v1:after",
"shopgate.catalog.getHighlightProducts.v1:after"
],
"input": [{"key": "products"}],
"output": [{"key": "products"}]
}
]
}
17 changes: 17 additions & 0 deletions extensions/@shopgate-theme-config/extension/mockEffectiveDates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Returns default configuration if request failed.
* @param {SDKContext} context context
* @param {Object} input input
* @returns {Promise<{products: Object[]}>}
*/
module.exports = async (context, { products = [] }) => ({
products: products.map((product) => {
// eslint-disable-next-line no-param-reassign
product.startDate = new Date(new Date().getTime() + 10000).toISOString();
// eslint-disable-next-line no-param-reassign
product.endDate = new Date(new Date().getTime() + 20000).toISOString();
// eslint-disable-next-line no-param-reassign
product.stock.info = 'Available in 2-3 days';
return product;
}),
});
15 changes: 15 additions & 0 deletions extensions/@shopgate-theme-config/frontend/config-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ export default {
'@shopgate/engage/product/OrderQuantityHint': {
show: true,
},
'@shopgate/engage/product/EffectivityDates': {
showStartDate: {
strategy: 'always',
daysBefore: null,
},
showEndDate: {
strategy: 'always',
daysBefore: null,
},
accessExpired: false,
},
},
pages: [
{
Expand Down Expand Up @@ -89,6 +100,10 @@ export default {
name: 'ShopgateProductOrderQuantityHint',
id: '@shopgate/engage/product/OrderQuantityHint',
},
{
name: 'ShopgateProductEffectivityDates',
id: '@shopgate/engage/product/EffectivityDates',
},
],
},
],
Expand Down
3 changes: 3 additions & 0 deletions extensions/@shopgate-theme-config/frontend/config-gmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default {
},
variables: {
baseShadow: 'rgba(0, 0, 0, .117647) 0 1px 6px, rgba(0, 0, 0, .117647) 0 1px 4px',
toast: {
duration: 5000,
},
},
shadows: {},
assets: {},
Expand Down
3 changes: 3 additions & 0 deletions extensions/@shopgate-theme-config/frontend/config-ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default {
},
variables: {
baseShadow: 'rgba(0, 0, 0, .117647) 0 1px 6px, rgba(0, 0, 0, .117647) 0 1px 4px',
toast: {
duration: 5000,
},
},
shadows: {},
assets: {},
Expand Down
15 changes: 15 additions & 0 deletions libraries/commerce/product/action-creators/productNotAvailable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PRODUCT_NOT_AVAILABLE } from '../constants';

/**
* Creates the dispatched PRODUCT_NOT_AVAILABLE action object.
* @param {string} productId Ids of the products to be set as expired.
* @param {string|Symbol} reason unavailable reason.
* @returns {Object} The dispatched action object.
*/
const productNotAvailable = (productId, reason) => ({
type: PRODUCT_NOT_AVAILABLE,
productId,
reason,
});

export default productNotAvailable;
5 changes: 5 additions & 0 deletions libraries/commerce/product/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ export const UPDATE_METADATA = 'UPDATE_METADATA';
// MEDIA TYPES
export const MEDIA_TYPE_IMAGE = 'image';
export const MEDIA_TYPE_VIDEO = 'video';

// PRODUCT NOT AVAILABLE + REASONS
export const PRODUCT_NOT_AVAILABLE = 'PRODUCT_NOT_AVAILABLE';
export const NOT_AVAILABLE_EFFECTIVITY_DATES = Symbol('EFFECTIVITY_DATES');

4 changes: 4 additions & 0 deletions libraries/commerce/product/streams/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ERROR_PRODUCT_PROPERTIES,
ERROR_PRODUCT_OPTIONS,
ERROR_PRODUCT_SHIPPING,
PRODUCT_NOT_AVAILABLE,
} from '../constants';

export const productWillEnter$ = routeWillEnter$.merge(routeDidUpdate$)
Expand Down Expand Up @@ -117,3 +118,6 @@ export const variantDidChange$ = variantWillUpdate$

export const productRelationsReceived$ =
main$.filter(({ action }) => action.type === RECEIVE_PRODUCT_RELATIONS);

export const productNotAvailable$ =
main$.filter(({ action }) => action.type === PRODUCT_NOT_AVAILABLE);
30 changes: 29 additions & 1 deletion libraries/commerce/product/subscriptions/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { hex2bin } from '@shopgate/pwa-common/helpers/data';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';
import { historyPop } from '@shopgate/pwa-common/actions/router';
import { historyPop, historyPush } from '@shopgate/engage/core';
import ToastProvider from '@shopgate/pwa-common/providers/toast';
import { getSearchRoute } from '@shopgate/pwa-common-commerce/search';
import fetchProduct from '../actions/fetchProduct';
import fetchProductDescription from '../actions/fetchProductDescription';
import fetchProductProperties from '../actions/fetchProductProperties';
Expand All @@ -18,11 +20,14 @@ import {
receivedVisibleProduct$,
errorProductResourcesNotFound$,
visibleProductNotFound$,
productNotAvailable$,
} from '../streams';
import fetchProductsById from '../actions/fetchProductsById';
import { getProductRelationsByHash } from '../selectors/relations';
import { checkoutSucceeded$ } from '../../checkout/streams';
import expireProductById from '../action-creators/expireProductById';
import { NOT_AVAILABLE_EFFECTIVITY_DATES } from '../constants';
import { getProductName } from '../selectors/product';

/**
* Product subscriptions.
Expand Down Expand Up @@ -110,6 +115,29 @@ function product(subscribe) {

dispatch(fetchProductsById(productIds));
});

const productNotAvailableEffDates$ = productNotAvailable$
.filter(({ action }) => action.reason === NOT_AVAILABLE_EFFECTIVITY_DATES);

/** Show toast with search action */
subscribe(productNotAvailableEffDates$, ({
action, getState, dispatch, events,
}) => {
const { productId } = action;
dispatch(expireProductById(productId));
dispatch(historyPop());
const name = getProductName(getState(), { productId });
events.emit(ToastProvider.ADD, {
id: 'product.available.not_search_similar',
message: 'product.available.not_search_similar',
messageParams: {
name,
},
action: () => dispatch(historyPush({
pathname: getSearchRoute(name),
})),
});
});
}

export default product;
6 changes: 3 additions & 3 deletions libraries/commerce/product/subscriptions/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import { ENOTFOUND } from '@shopgate/pwa-core';
import { mainSubject } from '@shopgate/pwa-common/store/middelwares/streams';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';
import { historyPop } from '@shopgate/pwa-common/actions/router';
import { historyPop } from '@shopgate/engage/core';
import expireProductById from '../action-creators/expireProductById';
import subscription from './index';
import { productRelationsReceived$, visibleProductNotFound$ } from '../streams';
import { ERROR_PRODUCT, RECEIVE_PRODUCT_CACHED } from '../constants';

const mockedGetProductsById = jest.fn();
jest.mock('@shopgate/pwa-common/actions/modal/showModal', () => jest.fn());
jest.mock('@shopgate/pwa-common/actions/router', () => ({
jest.mock('@shopgate/engage/core', () => ({
historyPop: jest.fn(),
}));
jest.mock('../actions/fetchProductsById', () => (...args) => mockedGetProductsById(...args));
Expand All @@ -26,7 +26,7 @@ describe('Product subscription', () => {
});

it('should subscribe', () => {
expect(subscribe).toHaveBeenCalledTimes(6);
expect(subscribe).toHaveBeenCalledTimes(7);
});

describe('productRelationsReceived$', () => {
Expand Down
3 changes: 3 additions & 0 deletions libraries/common/helpers/config/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const themeConfig = {
navbar: {
height: 56,
},
toast: {
duration: 5000,
},
navigator: {
height: 56,
shadow: materialShadow,
Expand Down
3 changes: 2 additions & 1 deletion libraries/common/helpers/i18n/getTranslator.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const getMessageFromCache = (locales, langCode, key) => {

messageCache[hash] = new IntlMessageFormat(
message,
langCode
langCode,
getPath(locales, 'formats')
);

return messageCache[hash];
Expand Down
7 changes: 7 additions & 0 deletions libraries/common/providers/toast/index.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { UIEvents } from '@shopgate/pwa-core';
import { themeConfig } from '@shopgate/pwa-common/helpers/config';
import ToastContext from './context';

const { variables: { toast: { duration = 5000 } = {} } = {} } = themeConfig;

/**
* The ToastProvider component
*/
Expand Down Expand Up @@ -62,12 +65,16 @@ class ToastProvider extends Component {
found.action = toast.action;
found.actionLabel = toast.actionLabel;
found.message = toast.message;
found.messageParams = toast.messageParams;
found.duration = toast.duration || duration;
} else {
toasts.push({
id: toast.id,
action: toast.action,
actionLabel: toast.actionLabel,
message: toast.message,
messageParams: toast.messageParams,
duration: toast.duration || duration,
});
}

Expand Down
1 change: 1 addition & 0 deletions libraries/common/subscriptions/router.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ let mockedWebCheckoutConfig = null;
jest.mock('@shopgate/pwa-common/helpers/config', () => ({
get shopCNAME() { return mockedShopCNAME; },
get webCheckoutShopify() { return mockedWebCheckoutConfig; },
themeConfig: {},
}));

jest.mock('@shopgate/pwa-core/helpers/logGroup', () => jest.fn());
Expand Down
1 change: 1 addition & 0 deletions libraries/engage/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export * from '@shopgate/pwa-core/helpers';
export { default as logGroup } from '@shopgate/pwa-core/helpers/logGroup';
export * from '@shopgate/pwa-core/helpers/version';
export * from '@shopgate/pwa-common/helpers/data';
export * from '@shopgate/pwa-common/helpers/date';
export * from '@shopgate/pwa-common/helpers/dom';
export * from '@shopgate/pwa-common/helpers/environment';
export { default as decodeHTML } from '@shopgate/pwa-common/helpers/html/decodeHTML';
Expand Down
33 changes: 33 additions & 0 deletions libraries/engage/product/components/EffectivityDates/connector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import {
makeGetProductEffectivityDates,
productNotAvailable,
NOT_AVAILABLE_EFFECTIVITY_DATES,
} from '@shopgate/engage/product';

/**
* Create exclusive component selector.
* @returns {Function}
*/
function makeMapStateToProps() {
const getProductEffectivityDates = makeGetProductEffectivityDates();

return (state, props) => ({
dates: getProductEffectivityDates(state, props),
});
}

/**
* Maps the contents of the state to the component props.
* @param {Function} dispatch The dispatch method from the store.
* @param {Object} props The props.
* @return {Object} The extended component props.
*/
const mapDispatchToProps = (dispatch, props) => ({
productNotAvailable: () => dispatch(productNotAvailable(
props.productId,
NOT_AVAILABLE_EFFECTIVITY_DATES
)),
});

export default connect(makeMapStateToProps, mapDispatchToProps);
68 changes: 68 additions & 0 deletions libraries/engage/product/components/EffectivityDates/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { isBefore, isAfter } from '@shopgate/engage/core';

/**
* Decide if startDate hint should be shown
* @param {Object} settings settings
* @param {Date} startDate product.startDate
* @returns {boolean}
*/
export const showStartDateHint = (settings, startDate) => {
if (!startDate || !startDate.getDate()) {
return false;
}

const {
showStartDate: {
strategy = 'always', // 'always|daysBefore|never',
daysBefore = 0,
} = {},
} = settings || {};

switch (strategy) {
case 'always':
return isBefore(Date.now(), startDate);

case 'daysBefore': {
const now = Date.now();
return isBefore(now, startDate)
&& isAfter(startDate, now.setDate(now.getDate() - daysBefore));
}

default:
case 'never':
return false;
}
};

/**
* Decide if endDate hint should be shown
* @param {Object} settings settings
* @param {Date} endDate product.endDate
* @returns {boolean}
*/
export const showEndDateHint = (settings, endDate) => {
if (!endDate || !endDate.getDate()) {
return false;
}

const {
showEndDate: {
strategy = 'always', // 'always|daysBefore|never',
daysBefore = 0,
} = {},
} = settings || {};

switch (strategy) {
case 'always':
return true;

case 'daysBefore': {
return isAfter(Date.now(), endDate.setDate(endDate.getDate() - daysBefore));
}

default:
case 'never':
return false;
}
};

Loading