Create Context Provider for Notices w/ Notices API (https://github.com/woocommerce/woocommerce-blocks/pull/1843)
* Working on store provider * Working on store provider * Reducer implementation * Implement core/notices * Add notices to store coupon hook with context * Improve store notice text and styling * Improve JS side API for notices * Wrap functions with context additon * Update test to [] * Implement props feedback and useInstanceId * Update assets/js/base/context/store-notices-context.js Co-Authored-By: Darren Ethier <darren@roughsmootheng.in> * Update assets/js/base/context/store-notices-context.js Co-Authored-By: Darren Ethier <darren@roughsmootheng.in> * remove instance id Co-authored-by: Darren Ethier <darren@roughsmootheng.in>
This commit is contained in:
parent
5fcf9b0fca
commit
d3a9dc3d6b
|
@ -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 (
|
||||
<div className={ wrapperClass }>
|
||||
{ notices.map( ( props ) => (
|
||||
<Notice
|
||||
key={ 'store-notice-' + props.id }
|
||||
{ ...props }
|
||||
className={ classnames(
|
||||
'wc-block-components-notices__notice',
|
||||
getWooClassName( props )
|
||||
) }
|
||||
onRemove={ () => {
|
||||
if ( props.isDismissible ) {
|
||||
removeNotice( props.id );
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ props.content }
|
||||
</Notice>
|
||||
) ) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StoreNoticesContainer.propTypes = {
|
||||
className: PropTypes.string,
|
||||
notices: PropTypes.array,
|
||||
};
|
||||
|
||||
export default StoreNoticesContainer;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<StoreNoticesContext.Provider value={ contextValue }>
|
||||
{ createNoticeContainer && (
|
||||
<StoreNoticesContainer
|
||||
className={ className }
|
||||
notices={ contextValue.notices }
|
||||
/>
|
||||
) }
|
||||
{ children }
|
||||
</StoreNoticesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
StoreNoticesProvider.propTypes = {
|
||||
className: PropTypes.string,
|
||||
createNoticeContainer: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
context: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StoreNoticesProvider;
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 (
|
||||
<>
|
||||
<div className="errors">
|
||||
{ // @todo This is a placeholder for error messages - this needs refactoring.
|
||||
cartErrors &&
|
||||
cartErrors.map( ( error = {}, i ) => (
|
||||
<div className="woocommerce-info" key={ 'notice-' + i }>
|
||||
{ error.message }
|
||||
</div>
|
||||
) ) }
|
||||
</div>
|
||||
<StoreNoticesProvider context="wc/cart">
|
||||
{ ! cartIsLoading && ! cartItems.length ? (
|
||||
<RawHTML>{ emptyCart }</RawHTML>
|
||||
) : (
|
||||
|
@ -54,7 +46,7 @@ const CartFrontend = ( {
|
|||
/>
|
||||
</LoadingMask>
|
||||
) }
|
||||
</>
|
||||
</StoreNoticesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -88,7 +88,7 @@ describe( 'getCollection', () => {
|
|||
'products',
|
||||
'?foo=bar',
|
||||
[ 20, 30 ],
|
||||
{ items: undefined, headers: undefined }
|
||||
{ items: [], headers: undefined }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue