diff --git a/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/index.js b/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/index.js
new file mode 100644
index 00000000000..0e2a6182dad
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/index.js
@@ -0,0 +1,59 @@
+/**
+ * External dependencies
+ */
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { Notice } from 'wordpress-components';
+import { useStoreNoticesContext } from '@woocommerce/base-context/store-notices-context';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+const getWooClassName = ( { status = 'default' } ) => {
+ switch ( status ) {
+ case 'error':
+ return 'woocommerce-message woocommerce-error';
+ case 'success':
+ return 'woocommerce-message woocommerce-success';
+ case 'info':
+ case 'warning':
+ return 'woocommerce-message woocommerce-info';
+ }
+ return '';
+};
+
+const StoreNoticesContainer = ( { className, notices } ) => {
+ const { removeNotice } = useStoreNoticesContext();
+ const wrapperClass = classnames( className, 'wc-block-components-notices' );
+
+ return (
+
+ { notices.map( ( props ) => (
+ {
+ if ( props.isDismissible ) {
+ removeNotice( props.id );
+ }
+ } }
+ >
+ { props.content }
+
+ ) ) }
+
+ );
+};
+
+StoreNoticesContainer.propTypes = {
+ className: PropTypes.string,
+ notices: PropTypes.array,
+};
+
+export default StoreNoticesContainer;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/style.scss
new file mode 100644
index 00000000000..31de746887c
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/store-notices-container/style.scss
@@ -0,0 +1,26 @@
+.wc-block-components-notices {
+ display: block;
+ margin-bottom: 2em;
+ .wc-block-components-notices__notice {
+ margin: 0;
+ .components-notice__content {
+ display: inline-block;
+ }
+ .components-notice__dismiss {
+ background: transparent none;
+ padding: 0;
+ margin: 0;
+ border: 0;
+ outline: 0;
+ color: #fff;
+ float: right;
+ svg {
+ fill: #fff;
+ vertical-align: text-top;
+ }
+ }
+ }
+ .wc-block-components-notices__notice + .wc-block-components-notices__notice {
+ margin-top: 1em;
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/base/context/store-notices-context.js b/plugins/woocommerce-blocks/assets/js/base/context/store-notices-context.js
new file mode 100644
index 00000000000..860d3ee4306
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/context/store-notices-context.js
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+import PropTypes from 'prop-types';
+import { createContext, useContext, useCallback } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+import StoreNoticesContainer from '@woocommerce/base-components/store-notices-container';
+
+const StoreNoticesContext = createContext( {
+ notices: [],
+ createNotice: () => void null,
+ removeNotice: () => void null,
+ context: 'wc/core',
+} );
+
+export const useStoreNoticesContext = () => {
+ return useContext( StoreNoticesContext );
+};
+
+/**
+ * Provides an interface for blocks to add notices to the frontend UI.
+ *
+ * Statuses map to https://github.com/WordPress/gutenberg/tree/master/packages/components/src/notice
+ * - Default (no status)
+ * - Error
+ * - Warning
+ * - Info
+ * - Success
+ */
+const StoreNoticesProvider = ( {
+ children,
+ className = '',
+ createNoticeContainer = true,
+ context = 'wc/core',
+} ) => {
+ const { createNotice, removeNotice } = useDispatch( 'core/notices' );
+
+ const createNoticeWithContext = useCallback(
+ ( status = 'default', content = '', options = {} ) => {
+ createNotice( status, content, {
+ ...options,
+ context: options.context || context,
+ } );
+ },
+ [ createNotice, context ]
+ );
+
+ const removeNoticeWithContext = useCallback(
+ ( id ) => {
+ removeNotice( id, context );
+ },
+ [ createNotice, context ]
+ );
+
+ const { notices } = useSelect(
+ ( select ) => {
+ return {
+ notices: select( 'core/notices' ).getNotices( context ),
+ };
+ },
+ [ context ]
+ );
+
+ const contextValue = {
+ notices,
+ createNotice: createNoticeWithContext,
+ removeNotice: removeNoticeWithContext,
+ context,
+ };
+
+ return (
+
+ { createNoticeContainer && (
+
+ ) }
+ { children }
+
+ );
+};
+
+StoreNoticesProvider.propTypes = {
+ className: PropTypes.string,
+ createNoticeContainer: PropTypes.bool,
+ children: PropTypes.node,
+ context: PropTypes.string,
+};
+
+export default StoreNoticesProvider;
diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/index.js b/plugins/woocommerce-blocks/assets/js/base/hooks/index.js
index 63c2e42b535..eb2820eabac 100644
--- a/plugins/woocommerce-blocks/assets/js/base/hooks/index.js
+++ b/plugins/woocommerce-blocks/assets/js/base/hooks/index.js
@@ -4,6 +4,7 @@ export * from './use-store-cart';
export * from './use-store-cart-coupons';
export * from './use-store-cart-item';
export * from './use-store-products';
+export * from './use-store-notices';
export * from './use-collection';
export * from './use-collection-header';
export * from './use-collection-data';
diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-coupons.js b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-coupons.js
index 80b6b6a53c6..e44fd499b3e 100644
--- a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-coupons.js
+++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-coupons.js
@@ -3,8 +3,10 @@
/**
* External dependencies
*/
+import { __, sprintf } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
+import { useStoreNotices } from '@woocommerce/base-hooks';
/**
* Internal dependencies
@@ -21,20 +23,80 @@ import { useStoreCart } from './use-store-cart';
*/
export const useStoreCartCoupons = () => {
const { cartCoupons, cartIsLoading } = useStoreCart();
+ const {
+ addErrorNotice,
+ addSuccessNotice,
+ addInfoNotice,
+ } = useStoreNotices();
- const results = useSelect( ( select, { dispatch } ) => {
- const store = select( storeKey );
- const isApplyingCoupon = store.isApplyingCoupon();
- const isRemovingCoupon = store.isRemovingCoupon();
- const { applyCoupon, removeCoupon } = dispatch( storeKey );
+ const results = useSelect(
+ ( select, { dispatch } ) => {
+ const store = select( storeKey );
+ const isApplyingCoupon = store.isApplyingCoupon();
+ const isRemovingCoupon = store.isRemovingCoupon();
+ const { applyCoupon, removeCoupon } = dispatch( storeKey );
- return {
- applyCoupon,
- removeCoupon,
- isApplyingCoupon,
- isRemovingCoupon,
- };
- }, [] );
+ const applyCouponWithNotices = ( couponCode ) => {
+ applyCoupon( couponCode )
+ .then( ( result ) => {
+ if ( result === true ) {
+ addSuccessNotice(
+ sprintf(
+ // translators: %s coupon code.
+ __(
+ 'Coupon code "%s" has been applied to your cart',
+ 'woo-gutenberg-products-block'
+ ),
+ couponCode
+ ),
+ {
+ id: 'coupon-form',
+ }
+ );
+ }
+ } )
+ .catch( ( error ) => {
+ addErrorNotice( error.message, {
+ id: 'coupon-form',
+ } );
+ } );
+ };
+
+ const removeCouponWithNotices = ( couponCode ) => {
+ removeCoupon( couponCode )
+ .then( ( result ) => {
+ if ( result === true ) {
+ addInfoNotice(
+ sprintf(
+ // translators: %s coupon code.
+ __(
+ 'Coupon code "%s" has been removed from your cart',
+ 'woo-gutenberg-products-block'
+ ),
+ couponCode
+ ),
+ {
+ id: 'coupon-form',
+ }
+ );
+ }
+ } )
+ .catch( ( error ) => {
+ addErrorNotice( error.message, {
+ id: 'coupon-form',
+ } );
+ } );
+ };
+
+ return {
+ applyCoupon: applyCouponWithNotices,
+ removeCoupon: removeCouponWithNotices,
+ isApplyingCoupon,
+ isRemovingCoupon,
+ };
+ },
+ [ addErrorNotice, addSuccessNotice, addInfoNotice ]
+ );
return {
appliedCoupons: cartCoupons,
diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-notices.js b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-notices.js
new file mode 100644
index 00000000000..922fd7d1dba
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-notices.js
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+import { useStoreNoticesContext } from '@woocommerce/base-context/store-notices-context';
+import { useMemo } from '@wordpress/element';
+
+export const useStoreNotices = () => {
+ const { notices, createNotice, removeNotice } = useStoreNoticesContext();
+
+ const noticesApi = useMemo(
+ () => ( {
+ addDefaultNotice: ( text, noticeProps = {} ) =>
+ void createNotice( 'default', text, {
+ ...noticeProps,
+ } ),
+ addErrorNotice: ( text, noticeProps = {} ) =>
+ void createNotice( 'error', text, {
+ ...noticeProps,
+ } ),
+ addWarningNotice: ( text, noticeProps = {} ) =>
+ void createNotice( 'warning', text, {
+ ...noticeProps,
+ } ),
+ addInfoNotice: ( text, noticeProps = {} ) =>
+ void createNotice( 'info', text, {
+ ...noticeProps,
+ } ),
+ addSuccessNotice: ( text, noticeProps = {} ) =>
+ void createNotice( 'success', text, {
+ ...noticeProps,
+ } ),
+ removeNotices: ( type = null ) => {
+ notices.map( ( notice ) => {
+ if ( type === null || notice.status === type ) {
+ removeNotice( notice.id );
+ }
+ return true;
+ } );
+ },
+ } ),
+ [ createNotice ]
+ );
+
+ return {
+ notices,
+ ...noticesApi,
+ };
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/frontend.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/frontend.js
index 769888851b2..712a3ddfd21 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/frontend.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/frontend.js
@@ -5,6 +5,8 @@ import { withRestApiHydration } from '@woocommerce/block-hocs';
import { useStoreCart } from '@woocommerce/base-hooks';
import { RawHTML } from '@wordpress/element';
import LoadingMask from '@woocommerce/base-components/loading-mask';
+import StoreNoticesProvider from '@woocommerce/base-context/store-notices-context';
+
/**
* Internal dependencies
*/
@@ -23,21 +25,11 @@ const CartFrontend = ( {
cartItems,
cartTotals,
cartIsLoading,
- cartErrors,
cartCoupons,
} = useStoreCart();
return (
- <>
-
- { // @todo This is a placeholder for error messages - this needs refactoring.
- cartErrors &&
- cartErrors.map( ( error = {}, i ) => (
-
- { error.message }
-
- ) ) }
-
+
{ ! cartIsLoading && ! cartItems.length ? (
{ emptyCart }
) : (
@@ -54,7 +46,7 @@ const CartFrontend = ( {
/>
) }
- >
+
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js
index 9b183a01b05..f0dd8d09227 100644
--- a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js
+++ b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js
@@ -72,6 +72,7 @@ export function receiveRemovingCoupon( couponCode ) {
* Applies a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be applied.
*
+ * @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to apply to the cart.
*/
export function* applyCoupon( couponCode ) {
@@ -90,17 +91,26 @@ export function* applyCoupon( couponCode ) {
if ( result ) {
yield receiveCart( result );
}
+
+ // Finished handling the coupon.
+ yield receiveApplyingCoupon( '' );
} catch ( error ) {
+ // Store the error message in state.
yield receiveError( error );
+ // Finished handling the coupon.
+ yield receiveApplyingCoupon( '' );
+ // Re-throw the error.
+ throw error;
}
- yield receiveApplyingCoupon( '' );
+ return true;
}
/**
* Removes a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be removed.
*
+ * @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to remove from the cart.
*/
export function* removeCoupon( couponCode ) {
@@ -119,11 +129,19 @@ export function* removeCoupon( couponCode ) {
if ( result ) {
yield receiveCart( result );
}
+
+ // Finished handling the coupon.
+ yield receiveRemovingCoupon( '' );
} catch ( error ) {
+ // Store the error message in state.
yield receiveError( error );
+ // Finished handling the coupon.
+ yield receiveRemovingCoupon( '' );
+ // Re-throw the error.
+ throw error;
}
- yield receiveRemovingCoupon( '' );
+ return true;
}
/**
diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js b/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js
index 9b2bc08e0d3..fa49835d064 100644
--- a/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js
+++ b/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js
@@ -7,13 +7,9 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
-import {
- receiveCollection,
- receiveCollectionError,
- DEFAULT_EMPTY_ARRAY,
-} from './actions';
+import { receiveCollection, receiveCollectionError } from './actions';
import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants';
-import { STORE_KEY } from './constants';
+import { STORE_KEY, DEFAULT_EMPTY_ARRAY } from './constants';
import { apiFetchWithHeaders } from './controls';
/**
diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js b/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js
index c0e8e7e96c4..f1375571474 100644
--- a/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js
+++ b/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js
@@ -88,7 +88,7 @@ describe( 'getCollection', () => {
'products',
'?foo=bar',
[ 20, 30 ],
- { items: undefined, headers: undefined }
+ { items: [], headers: undefined }
)
);
}
diff --git a/plugins/woocommerce-blocks/assets/js/data/index.js b/plugins/woocommerce-blocks/assets/js/data/index.js
index a31c17f1463..4c2a85ee85f 100644
--- a/plugins/woocommerce-blocks/assets/js/data/index.js
+++ b/plugins/woocommerce-blocks/assets/js/data/index.js
@@ -1,3 +1,11 @@
+/**
+ * External dependencies
+ */
+import '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
export { SCHEMA_STORE_KEY } from './schema';
export { COLLECTIONS_STORE_KEY } from './collections';
export { CART_STORE_KEY } from './cart';
diff --git a/plugins/woocommerce-blocks/package-lock.json b/plugins/woocommerce-blocks/package-lock.json
index 9bdbe39ec7e..1a44ffe928c 100644
--- a/plugins/woocommerce-blocks/package-lock.json
+++ b/plugins/woocommerce-blocks/package-lock.json
@@ -5100,6 +5100,18 @@
}
}
},
+ "@wordpress/notices": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-1.12.0.tgz",
+ "integrity": "sha512-TSX9ih2LfInO+/v0lb1k1PBOHYveIKINkLAmD+BJtAgFVjbJG1465rinv+efAYiqcnmQhrHHrpn4wGUP/7c0jg==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.8.3",
+ "@wordpress/a11y": "^2.7.0",
+ "@wordpress/data": "^4.13.0",
+ "lodash": "^4.17.15"
+ }
+ },
"@wordpress/viewport": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@wordpress/viewport/-/viewport-2.13.0.tgz",
@@ -5409,14 +5421,13 @@
}
},
"@wordpress/notices": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-1.12.0.tgz",
- "integrity": "sha512-TSX9ih2LfInO+/v0lb1k1PBOHYveIKINkLAmD+BJtAgFVjbJG1465rinv+efAYiqcnmQhrHHrpn4wGUP/7c0jg==",
- "dev": true,
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-2.0.0.tgz",
+ "integrity": "sha512-NOkI8r2YLRxRrx+z7wDzpifyJAMnKezVjTnsy6EiU84Kai7FM2Ce+I51asw3c6UfLPpDY8DjLrzUWVZkhQF/og==",
"requires": {
"@babel/runtime": "^7.8.3",
"@wordpress/a11y": "^2.7.0",
- "@wordpress/data": "^4.13.0",
+ "@wordpress/data": "^4.14.0",
"lodash": "^4.17.15"
}
},
@@ -29654,6 +29665,40 @@
}
}
},
+ "wordpress-compose": {
+ "version": "npm:@wordpress/compose@3.11.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-3.11.0.tgz",
+ "integrity": "sha512-CNbLn9NtG2A0X71wjEux126uEHpWp3v546FtSgMoWlq73z3LEEBDoEeS2glIPAbIK6e1X2UibsKrn5Tn651tlg==",
+ "requires": {
+ "@babel/runtime": "^7.8.3",
+ "@wordpress/element": "^2.11.0",
+ "@wordpress/is-shallow-equal": "^1.8.0",
+ "lodash": "^4.17.15",
+ "mousetrap": "^1.6.2"
+ },
+ "dependencies": {
+ "@wordpress/element": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-2.11.0.tgz",
+ "integrity": "sha512-56ZO8a+E7QEsYwiqS+3BQPSHrCPsOAIEz5smXzntb2f6BjvOKeA64pup40mdn1pNGexe06LBA8cjoZVdLBHB1w==",
+ "requires": {
+ "@babel/runtime": "^7.8.3",
+ "@wordpress/escape-html": "^1.7.0",
+ "lodash": "^4.17.15",
+ "react": "^16.9.0",
+ "react-dom": "^16.9.0"
+ }
+ },
+ "@wordpress/is-shallow-equal": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-1.8.0.tgz",
+ "integrity": "sha512-OV3qJqP9LhjuOzt85TsyBwv+//CvC8Byf/81D3NmjPKlstLaD/bBCC5nBhH6dKAv4bShYtQ2Hmut+V4dZnOM1A==",
+ "requires": {
+ "@babel/runtime": "^7.8.3"
+ }
+ }
+ }
+ },
"wordpress-element": {
"version": "npm:@wordpress/element@2.11.0",
"resolved": "https://registry.npmjs.org/@wordpress/element/-/element-2.11.0.tgz",
diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json
index 1fdfe18c82c..710ce90d601 100644
--- a/plugins/woocommerce-blocks/package.json
+++ b/plugins/woocommerce-blocks/package.json
@@ -141,6 +141,7 @@
},
"dependencies": {
"@woocommerce/components": "4.0.0",
+ "@wordpress/notices": "^2.0.0",
"classnames": "2.2.6",
"compare-versions": "3.6.0",
"config": "3.2.6",
@@ -149,7 +150,8 @@
"trim-html": "0.1.9",
"use-debounce": "3.3.0",
"wordpress-components": "npm:@wordpress/components@8.5.0",
- "wordpress-element": "npm:@wordpress/element@2.11.0"
+ "wordpress-element": "npm:@wordpress/element@2.11.0",
+ "wordpress-compose": "npm:@wordpress/compose@3.11.0"
},
"husky": {
"hooks": {
diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php
index cce572dc105..12be4d38dfb 100644
--- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php
@@ -228,7 +228,11 @@ class CartController {
if ( $coupon->get_code() !== $coupon_code ) {
throw new RestException(
'woocommerce_rest_cart_coupon_error',
- __( 'Invalid coupon code.', 'woo-gutenberg-products-block' ),
+ sprintf(
+ /* Translators: %s coupon code */
+ __( '"%s" is an invalid coupon code.', 'woo-gutenberg-products-block' ),
+ esc_html( $coupon_code )
+ ),
403
);
}
@@ -236,7 +240,11 @@ class CartController {
if ( $this->has_coupon( $coupon_code ) ) {
throw new RestException(
'woocommerce_rest_cart_coupon_error',
- __( 'Coupon has already been applied.', 'woo-gutenberg-products-block' ),
+ sprintf(
+ /* Translators: %s coupon code */
+ __( 'Coupon code "%s" has already been applied.', 'woo-gutenberg-products-block' ),
+ esc_html( $coupon_code )
+ ),
403
);
}