Skip to content

Commit

Permalink
Merge pull request #1217 from shopgate/CURB-3885-back-in-stock-feature
Browse files Browse the repository at this point in the history
Implemented Back-In-Stock reminder
  • Loading branch information
fkloes committed Mar 12, 2024
2 parents 79516c7 + 181e019 commit 23aa2b7
Show file tree
Hide file tree
Showing 103 changed files with 1,829 additions and 831 deletions.
1 change: 1 addition & 0 deletions libraries/common/helpers/config/__mocks__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const themeColors = themeConfig.colors;
export const themeShadows = themeConfig.shadows;
export const themeVariables = themeConfig.variables;
export const themeIcons = themeConfig.icons;
export const themeName = 'theme';

const appConfig = {
get hideProductImageShadow() { return true; },
Expand Down
3 changes: 2 additions & 1 deletion libraries/common/helpers/config/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const variables = {
loadingIndicator: {
size: 32,
strokeWidth: 3,
imgSrc: null,
},
paymentBar: {
height: 78,
Expand Down Expand Up @@ -207,4 +208,4 @@ export const themeConfig = {

// Alias for jest hoisting
export const mockThemeConfig = themeConfig;

export const themeName = 'theme';
114 changes: 114 additions & 0 deletions libraries/engage/back-in-stock/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { PipelineRequest } from '@shopgate/engage/core';
import {
SHOPGATE_USER_ADD_BACK_IN_STOCK_SUBSCRIPTION,
SHOPGATE_USER_DELETE_BACK_IN_STOCK_SUBSCRIPTION,
SHOPGATE_USER_GET_BACK_IN_STOCK_SUBSCRIPTIONS,
ADD_BACK_IN_STOCK_SUBSCRIPTION,
ADD_BACK_IN_STOCK_SUBSCRIPTION_ERROR,
ADD_BACK_IN_STOCK_SUBSCRIPTION_SUCCESS,
FETCH_BACK_IN_STOCK_SUBSCRIPTIONS,
FETCH_BACK_IN_STOCK_SUBSCRIPTIONS_ERROR,
FETCH_BACK_IN_STOCK_SUBSCRIPTIONS_SUCCESS,
REMOVE_BACK_IN_STOCK_SUBSCRIPTION,
REMOVE_BACK_IN_STOCK_SUBSCRIPTION_ERROR,
REMOVE_BACK_IN_STOCK_SUBSCRIPTION_SUCCESS,
} from '@shopgate/engage/back-in-stock/constants';

/**
* Fetch Back in Stock Subscriptions
* @returns {Function}
*/
export const fetchBackInStockSubscriptions = () => (dispatch) => {
dispatch({ type: FETCH_BACK_IN_STOCK_SUBSCRIPTIONS });

const request = new PipelineRequest(SHOPGATE_USER_GET_BACK_IN_STOCK_SUBSCRIPTIONS)
.setInput({
limit: 100,
filters: {
status: { $in: ['active', 'triggered'] },
},
})
.setRetries(0)
.dispatch();

request
.then(({ subscriptions }) => {
dispatch({
type: FETCH_BACK_IN_STOCK_SUBSCRIPTIONS_SUCCESS,
subscriptions,
});
})
.catch((error) => {
dispatch({
type: FETCH_BACK_IN_STOCK_SUBSCRIPTIONS_ERROR,
error,
});
});

return request;
};

/**
* Add a Back in Stock Subscription
* @param {Object} props Props.
* @param {string} props.productId The product for which the subscription should be added
* @returns {Function}
*/
export const addBackInStockSubscription = ({ productId }) => (dispatch) => {
dispatch({ type: ADD_BACK_IN_STOCK_SUBSCRIPTION });
const request = new PipelineRequest(SHOPGATE_USER_ADD_BACK_IN_STOCK_SUBSCRIPTION)
.setInput({
productCode: productId,
})
.setRetries(0)
.dispatch();

request
.then(({ subscriptions }) => {
dispatch({
type: ADD_BACK_IN_STOCK_SUBSCRIPTION_SUCCESS,
subscriptions,
});
})
.catch((error) => {
dispatch({
type: ADD_BACK_IN_STOCK_SUBSCRIPTION_ERROR,
error,
});
});

return request;
};

/**
* Remove a Back in Stock Subscription
* @param {Object} props Props.
* @param {string} props.subscriptionCode The subscription which should be deleted
* @returns {Function}
*/
export const removeBackInStockSubscription = ({ subscriptionCode }) => (dispatch) => {
dispatch({ type: REMOVE_BACK_IN_STOCK_SUBSCRIPTION });
const request = new PipelineRequest(SHOPGATE_USER_DELETE_BACK_IN_STOCK_SUBSCRIPTION)
.setInput({
subscriptionCode,
})
.setRetries(0)
.dispatch();

request
.then(({ subscriptions }) => {
dispatch({
type: REMOVE_BACK_IN_STOCK_SUBSCRIPTION_SUCCESS,
subscriptions,
});
})
.catch((error) => {
dispatch({
type: REMOVE_BACK_IN_STOCK_SUBSCRIPTION_ERROR,
error,
});
});

return request;
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { addBackInStockSubscription } from '@shopgate/engage/back-in-stock/actions';
import grantPushPermissions from '@shopgate/engage/core/actions/grantPushPermissions';

const mapDispatchToProps = {
addBackInStockSubscription,
grantPushPermissions,
};

export default connect(null, mapDispatchToProps);
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { themeConfig } from '@shopgate/engage';
import {
Link,
CheckedIcon,
NotificationIcon,
} from '@shopgate/engage/components';
import { BACK_IN_STOCK_PATTERN } from '@shopgate/engage/back-in-stock/constants';
import { i18n } from '@shopgate/engage/core';
import styles from './style';
import connect from './connector';

const { colors } = themeConfig;
/**
* This component renders a button to subscribe a product or a hint
* that the product is already subscribed
* @param {Object} props The component props
* @param {boolean} props.isLinkToBackInStockEnabled Whether the link to the back in
* stock page is active
* @param {boolean} props.stopPropagation Stop event propagation
* @param {string} props.productId The product id
* @param {Object} props.subscription The subscription
* @param {Function} props.addBackInStockSubscription Add product to back in stock list
* @param {Function} props.grantPushPermissions Request / Set push permission
* @return {JSX}
*/
const BackInStockButton = ({
productId,
isLinkToBackInStockEnabled = false,
subscription,
stopPropagation = false,
addBackInStockSubscription,
grantPushPermissions,
}) => {
const handleClick = useCallback(async (event) => {
if (stopPropagation) {
event.stopPropagation();
}
const allowed = await grantPushPermissions({
rationaleModal: {
message: 'permissions.back_in_stock_push_notifications.message',
confirm: 'permissions.back_in_stock_push_notifications.confirm',
dismiss: 'permissions.back_in_stock_push_notifications.dismiss',
},
});
if (allowed) {
addBackInStockSubscription({ productId });
}
}, [addBackInStockSubscription, grantPushPermissions, productId, stopPropagation]);

if (subscription?.status === 'active') {
return (
<Link
href={BACK_IN_STOCK_PATTERN}
disabled={!isLinkToBackInStockEnabled}
className={styles.backInStockMessageContainer}
tag="span"
>
<CheckedIcon color={colors.success} className={styles.icon} />
<span className={styles.backInStockMessage}>{i18n.text('back_in_stock.we_will_remind_you')}</span>
</Link>
);
}

return (
<div>
{/* eslint-disable-next-line max-len */}
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid,jsx-a11y/interactive-supports-focus,jsx-a11y/click-events-have-key-events */}
<a role="button" onClick={handleClick} className={styles.button}>
<div className={styles.buttonContent}>
<NotificationIcon color={colors.primary} />
<span className={styles.buttonText}>{i18n.text('back_in_stock.get_notified')}</span>
</div>
</a>
</div>);
};

BackInStockButton.propTypes = {
addBackInStockSubscription: PropTypes.func.isRequired,
grantPushPermissions: PropTypes.func.isRequired,
isLinkToBackInStockEnabled: PropTypes.bool,
productId: PropTypes.string,
stopPropagation: PropTypes.bool,
subscription: PropTypes.shape(),
};

BackInStockButton.defaultProps = {
stopPropagation: false,
isLinkToBackInStockEnabled: false,
subscription: null,
productId: null,
};

export default connect(BackInStockButton);
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { css } from 'glamor';
import { themeConfig } from '@shopgate/engage';

export default {
button: css({
color: themeConfig.colors.primary,
}).toString(),
buttonContent: css({
display: 'flex',
alignItems: 'center',
}).toString(),
backInStockMessageContainer: css({
lineHeight: '20px',
display: 'flex',
alignItems: 'center',
textAlign: 'end',
width: 'auto',
}).toString(),
backInStockMessage: css({
marginLeft: '4px',
verticalAlign: 'middle',
fontSize: '0.875rem',
}).toString(),
buttonText: css({
marginLeft: '4px',
fontSize: '0.875rem',
lineHeight: '16.5px',
}).toString(),
icon: css({
verticalAlign: 'middle',
display: 'inline',
}).toString(),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import {
getIsBackInStockEnabled,
makeGetSubscriptionByCharacteristics,
} from '@shopgate/engage/back-in-stock/selectors';
import {
makeGetProductByCharacteristics,
} from '@shopgate/engage/product';

/**
* @return {Object} The extended component props.
*/
const makeMapStateToProps = () => {
const getProductByCharacteristics = makeGetProductByCharacteristics({ strict: true });
const getSubscriptionByCharacteristics = makeGetSubscriptionByCharacteristics();

return (state, props) => {
const variant = getProductByCharacteristics(state, props) || {};
return ({
variant,
subscription: getSubscriptionByCharacteristics(state, props),
isBackInStockEnabled: getIsBackInStockEnabled(state, props),
});
};
};

export default connect(makeMapStateToProps);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { BackInStockButton } from '@shopgate/engage/back-in-stock/components';
import { withCurrentProduct } from '@shopgate/engage/core';
import connect from './connector';

/**
* The CharacteristicsButton component.
* @param {Object} props The component props.
* @param {boolean} props.isBackInStockEnabled Whether the back in stock feature is enabled
* @param {Object} props.variant The variant for this entry
* @param {Object} props.subscription The subscription
* @return {JSX}
*/
const CharacteristicsButton = ({
isBackInStockEnabled,
subscription,
variant,
}) => {
const productIsNotAVariant = !variant;
const featureIsNotEnabled = !isBackInStockEnabled;
const productIsNotAvailable = variant?.stock?.quantity === 0 &&
variant?.stock?.ignoreQuantity === false;

if (productIsNotAVariant || featureIsNotEnabled || !productIsNotAvailable) return null;

return (
<div style={{
display: 'flex',
justifyContent: 'end',
}}
>
<BackInStockButton
subscription={subscription}
stopPropagation
productId={variant.id}
/>
</div>);
};

CharacteristicsButton.propTypes = {
isBackInStockEnabled: PropTypes.bool.isRequired,
subscription: PropTypes.shape(),
variant: PropTypes.shape(),
};

CharacteristicsButton.defaultProps = {
variant: {},
subscription: null,
};

export default withCurrentProduct(connect(CharacteristicsButton));
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import {
getIsBackInStockEnabled,
makeGetSubscriptionByProduct,
} from '@shopgate/engage/back-in-stock/selectors';
import {
getProduct,
} from '@shopgate/engage/product';

/**
* @return {Object} The extended component props.
*/
const makeMapStateToProps = () => {
const getSubscriptionByProduct = makeGetSubscriptionByProduct();
return (state, props) => ({
subscription: getSubscriptionByProduct(state, props),
isBackInStockEnabled: getIsBackInStockEnabled(state, props),
product: getProduct(state, props),
});
};

export default connect(makeMapStateToProps);
Loading

0 comments on commit 23aa2b7

Please sign in to comment.