diff --git a/assets/js/atomic/blocks/index.js b/assets/js/atomic/blocks/index.js index 5a1ff791cbd..6745263834d 100644 --- a/assets/js/atomic/blocks/index.js +++ b/assets/js/atomic/blocks/index.js @@ -13,3 +13,4 @@ import './product-elements/category-list'; import './product-elements/tag-list'; import './product-elements/stock-indicator'; import './product-elements/add-to-cart'; +import './product-elements/product-image-gallery'; diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json b/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json new file mode 100644 index 00000000000..0a1247cbc34 --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json @@ -0,0 +1,17 @@ +{ + "name": "woocommerce/product-image-gallery", + "version": "1.0.0", + "title": "Product Image Gallery", + "icon": "gallery", + "description": "Display a product's images.", + "category": "woocommerce", + "supports": { + "align": true, + "reusable": false + }, + "keywords": [ "WooCommerce" ], + "usesContext": [ "postId", "postType", "queryId" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx b/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx new file mode 100644 index 00000000000..df6fb8ed95e --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings'; +import { isEmptyObject } from '@woocommerce/types'; +import { useBlockProps } from '@wordpress/block-editor'; +import { BlockAttributes } from '@wordpress/blocks'; +import { Disabled } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './editor.scss'; + +const Placeholder = () => { + return ( +
+ Placeholder +
+ { [ ...Array( 4 ).keys() ].map( ( index ) => { + return ( + Placeholder + ); + } ) } +
+
+ ); +}; + +type Context = { + postId: string; + postType: string; + queryId: string; +}; + +interface Props { + attributes: BlockAttributes; + context: Context; +} + +const Edit = ( { context }: Props ) => { + const blockProps = useBlockProps(); + + if ( isEmptyObject( context ) ) { + return ( +
+ + + +
+ ); + } + // We have work on this case when we will work on the Single Product block. + return ''; +}; + +export default Edit; diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss b/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss new file mode 100644 index 00000000000..40696afdbba --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss @@ -0,0 +1,13 @@ +.wc-block-editor-product-gallery { + img { + width: 500px; + height: 500px; + } + .wc-block-editor-product-gallery__other-images { + img { + width: 100px; + height: 100px; + margin: 5px; + } + } +} diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts b/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts new file mode 100644 index 00000000000..ca81e18eee9 --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { gallery as icon } from '@wordpress/icons'; +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; +import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import metadata from './block.json'; + +registerBlockSingleProductTemplate( { + registerBlockFn: () => { + // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. + registerBlockType( metadata, { + icon, + edit, + } ); + }, + unregisterBlockFn: () => { + unregisterBlockType( metadata.name ); + }, + blockName: metadata.name, +} ); diff --git a/assets/js/atomic/utils/index.js b/assets/js/atomic/utils/index.js index f10a14b6858..06280bb433f 100644 --- a/assets/js/atomic/utils/index.js +++ b/assets/js/atomic/utils/index.js @@ -2,3 +2,4 @@ export * from './get-block-map'; export * from './create-blocks-from-template'; export * from './render-parent-block'; export * from './render-standalone-blocks'; +export * from './register-block-single-product-template'; diff --git a/assets/js/atomic/utils/register-block-single-product-template.ts b/assets/js/atomic/utils/register-block-single-product-template.ts new file mode 100644 index 00000000000..675e335b06d --- /dev/null +++ b/assets/js/atomic/utils/register-block-single-product-template.ts @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { getBlockType } from '@wordpress/blocks'; +import { subscribe, select } from '@wordpress/data'; + +export const registerBlockSingleProductTemplate = ( { + registerBlockFn, + unregisterBlockFn, + blockName, +}: { + registerBlockFn: () => void; + unregisterBlockFn: () => void; + blockName: string; +} ) => { + let currentTemplateId: string | undefined; + + subscribe( () => { + const previousTemplateId = currentTemplateId; + const store = select( 'core/edit-site' ); + currentTemplateId = store?.getEditedPostId() as string | undefined; + + if ( previousTemplateId === currentTemplateId ) { + return; + } + + const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ]; + + if ( parsedTemplate === null || parsedTemplate === undefined ) { + return; + } + + const block = getBlockType( blockName ); + + if ( + block === undefined && + parsedTemplate.includes( 'single-product' ) + ) { + registerBlockFn(); + } + + if ( block !== undefined ) { + unregisterBlockFn(); + } + }, 'core/edit-site' ); +}; diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 364fd819d9f..3a211ee37fe 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -22,10 +22,8 @@ import { ShippingAddress, } from '@woocommerce/settings'; import { useSelect, useDispatch } from '@wordpress/data'; -import { - VALIDATION_STORE_KEY, - FieldValidationStatus, -} from '@woocommerce/block-data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { FieldValidationStatus } from '@woocommerce/types'; /** * Internal dependencies diff --git a/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx b/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx index c11a16eb2a4..968192ed6b6 100644 --- a/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx +++ b/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx @@ -10,10 +10,7 @@ import { getCurrencyFromPriceResponse, formatPrice, } from '@woocommerce/price-format'; -import { - __experimentalApplyCheckoutFilter, - mustContain, -} from '@woocommerce/blocks-checkout'; +import { applyCheckoutFilter, mustContain } from '@woocommerce/blocks-checkout'; import Dinero from 'dinero.js'; import { getSetting } from '@woocommerce/settings'; import { useMemo } from '@wordpress/element'; @@ -52,7 +49,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => { extensions, } = cartItem; - // Prepare props to pass to the __experimentalApplyCheckoutFilter filter. + // Prepare props to pass to the applyCheckoutFilter filter. // We need to pluck out receiveCart. // eslint-disable-next-line no-unused-vars const { receiveCart, ...cart } = useStoreCart(); @@ -68,7 +65,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => { const priceCurrency = getCurrencyFromPriceResponse( prices ); - const name = __experimentalApplyCheckoutFilter( { + const name = applyCheckoutFilter( { filterName: 'itemName', defaultValue: initialName, extensions, @@ -101,7 +98,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => { amount: lineSubtotal, precision: totalsCurrency.minorUnit, } ).getAmount(); - const subtotalPriceFormat = __experimentalApplyCheckoutFilter( { + const subtotalPriceFormat = applyCheckoutFilter( { filterName: 'subtotalPriceFormat', defaultValue: '', extensions, @@ -110,7 +107,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => { } ); // Allow extensions to filter how the price is displayed. Ie: prepending or appending some values. - const productPriceFormat = __experimentalApplyCheckoutFilter( { + const productPriceFormat = applyCheckoutFilter( { filterName: 'cartItemPrice', defaultValue: '', extensions, @@ -118,7 +115,7 @@ const OrderSummaryItem = ( { cartItem }: OrderSummaryProps ): JSX.Element => { validation: productPriceValidation, } ); - const cartItemClassNameFilter = __experimentalApplyCheckoutFilter( { + const cartItemClassNameFilter = applyCheckoutFilter( { filterName: 'cartItemClass', defaultValue: '', extensions, diff --git a/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx b/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx index 108fb33f25a..204596bffd0 100644 --- a/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx +++ b/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx @@ -30,10 +30,9 @@ const ShippingCalculator = ( { addressFields = [ 'country', 'state', 'city', 'postcode' ], }: ShippingCalculatorProps ): JSX.Element => { const { shippingAddress } = useCustomerData(); - const noticeContext = 'wc/cart/shipping-calculator'; return (
- + - { __( - 'Apply a coupon code', - 'woo-gutenberg-products-block' - ) } + { __( 'Add a coupon', 'woo-gutenberg-products-block' ) } ) } > => { - const observerResponses = []; +): Promise< ObserverResponse[] > => { + const observerResponses: ObserverResponse[] = []; const observersByType = getObserversByPriority( observers, eventType ); for ( const observer of observersByType ) { try { const response = await Promise.resolve( observer.callback( data ) ); - if ( typeof response !== 'object' || response === null ) { + if ( ! isObserverResponse( response ) ) { continue; } if ( ! response.hasOwnProperty( 'type' ) ) { @@ -90,7 +93,7 @@ export const emitEventWithAbort = async ( // We don't handle thrown errors but just console.log for troubleshooting. // eslint-disable-next-line no-console console.error( e ); - observerResponses.push( { type: 'error' } ); + observerResponses.push( { type: responseTypes.ERROR } ); return observerResponses; } } diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index 7cb4a5c036e..f9749e2d018 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isObject } from '@woocommerce/types'; +import { FieldValidationStatus, isObject } from '@woocommerce/types'; /** * Internal dependencies @@ -42,6 +42,16 @@ export interface ResponseType extends Record< string, unknown > { retry?: boolean; } +/** + * Observers of checkout/cart events can return a response object to indicate success/error/failure. They may also + * optionally pass metadata. + */ +export interface ObserverResponse { + type: responseTypes; + meta?: Record< string, unknown > | undefined; + validationErrors?: Record< string, FieldValidationStatus > | undefined; +} + const isResponseOf = ( response: unknown, type: string @@ -51,19 +61,27 @@ const isResponseOf = ( export const isSuccessResponse = ( response: unknown -): response is ResponseType => { +): response is ObserverFailResponse => { return isResponseOf( response, responseTypes.SUCCESS ); }; - +interface ObserverSuccessResponse extends ObserverResponse { + type: responseTypes.SUCCESS; +} export const isErrorResponse = ( response: unknown -): response is ResponseType => { +): response is ObserverSuccessResponse => { return isResponseOf( response, responseTypes.ERROR ); }; +interface ObserverErrorResponse extends ObserverResponse { + type: responseTypes.ERROR; +} +interface ObserverFailResponse extends ObserverResponse { + type: responseTypes.FAIL; +} export const isFailResponse = ( response: unknown -): response is ResponseType => { +): response is ObserverErrorResponse => { return isResponseOf( response, responseTypes.FAIL ); }; diff --git a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts index 2923fb7e031..81a25490dc4 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts @@ -6,7 +6,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data'; import { decodeEntities } from '@wordpress/html-entities'; import type { StoreCartCoupon } from '@woocommerce/types'; -import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; +import { applyCheckoutFilter } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -46,7 +46,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { return applyCoupon( couponCode ) .then( () => { if ( - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName: 'showApplyCouponNotice', defaultValue: true, arg: { couponCode, context }, @@ -86,7 +86,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { return removeCoupon( couponCode ) .then( () => { if ( - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName: 'showRemoveCouponNotice', defaultValue: true, arg: { couponCode, context }, diff --git a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts index e8e2b720eb2..0a8ae83c3a8 100644 --- a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts +++ b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts @@ -60,13 +60,46 @@ export const usePaymentMethodInterface = (): PaymentMethodInterface => { return { // The paymentStatus is exposed to third parties via the payment method interface so the API must not be changed paymentStatus: { - isPristine: store.isPaymentPristine(), - isStarted: store.isPaymentStarted(), + get isPristine() { + deprecated( 'isPristine', { + since: '9.6.0', + alternative: 'isIdle', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return store.isPaymentIdle(); + }, // isPristine is the same as isIdle + isIdle: store.isPaymentIdle(), + isStarted: store.isExpressPaymentStarted(), isProcessing: store.isPaymentProcessing(), - isFinished: store.isPaymentFinished(), + get isFinished() { + deprecated( 'isFinished', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return ( + store.hasPaymentError() || store.isPaymentReady() + ); + }, hasError: store.hasPaymentError(), - hasFailed: store.isPaymentFailed(), - isSuccessful: store.isPaymentSuccess(), + get hasFailed() { + deprecated( 'hasFailed', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return store.hasPaymentError(); + }, + get isSuccessful() { + deprecated( 'isSuccessful', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return store.isPaymentReady(); + }, + isReady: store.isPaymentReady(), isDoingExpressPayment: store.isExpressPaymentMethodActive(), }, activePaymentMethod: store.getActivePaymentMethod(), diff --git a/assets/js/base/context/providers/cart-checkout/checkout-processor.ts b/assets/js/base/context/providers/cart-checkout/checkout-processor.ts index 7024107f250..0428c215b5b 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-processor.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-processor.ts @@ -92,7 +92,7 @@ const CheckoutProcessor = () => { paymentMethodData, isExpressPaymentMethodActive, hasPaymentError, - isPaymentSuccess, + isPaymentReady, shouldSavePayment, } = useSelect( ( select ) => { const store = select( PAYMENT_STORE_KEY ); @@ -102,7 +102,7 @@ const CheckoutProcessor = () => { paymentMethodData: store.getPaymentMethodData(), isExpressPaymentMethodActive: store.isExpressPaymentMethodActive(), hasPaymentError: store.hasPaymentError(), - isPaymentSuccess: store.isPaymentSuccess(), + isPaymentReady: store.isPaymentReady(), shouldSavePayment: store.getShouldSavePaymentMethod(), }; }, [] ); @@ -130,7 +130,7 @@ const CheckoutProcessor = () => { const paidAndWithoutErrors = ! checkoutHasError && ! checkoutWillHaveError && - ( isPaymentSuccess || ! cartNeedsPayment ) && + ( isPaymentReady || ! cartNeedsPayment ) && checkoutIsProcessing; // Determine if checkout has an error. diff --git a/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx b/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx index 10a38b2cea2..d9b61713597 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx +++ b/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx @@ -61,16 +61,18 @@ export const PaymentEventsProvider = ( { isCalculating: store.isCalculating(), }; } ); - const { isPaymentSuccess, isPaymentFinished, isPaymentProcessing } = - useSelect( ( select ) => { - const store = select( PAYMENT_STORE_KEY ); + const { isPaymentReady } = useSelect( ( select ) => { + const store = select( PAYMENT_STORE_KEY ); - return { - isPaymentSuccess: store.isPaymentSuccess(), - isPaymentFinished: store.isPaymentFinished(), - isPaymentProcessing: store.isPaymentProcessing(), - }; - } ); + return { + // The PROCESSING status represents befor the checkout runs the observers + // registered for the payment_setup event. + isPaymentProcessing: store.isPaymentProcessing(), + // the READY status represents when the observers have finished processing and payment data + // synced with the payment store, ready to be sent to the StoreApi + isPaymentReady: store.isPaymentReady(), + }; + } ); const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const [ observers, observerDispatch ] = useReducer( emitReducer, {} ); @@ -84,59 +86,50 @@ export const PaymentEventsProvider = ( { const { __internalSetPaymentProcessing, - __internalSetPaymentPristine, + __internalSetPaymentIdle, __internalEmitPaymentProcessingEvent, } = useDispatch( PAYMENT_STORE_KEY ); - // flip payment to processing if checkout processing is complete, there are no errors, and payment status is started. + // flip payment to processing if checkout processing is complete and there are no errors useEffect( () => { if ( checkoutIsProcessing && ! checkoutHasError && - ! checkoutIsCalculating && - ! isPaymentFinished + ! checkoutIsCalculating ) { __internalSetPaymentProcessing(); + + // Note: the nature of this event emitter is that it will bail on any + // observer that returns a response that !== true. However, this still + // allows for other observers that return true for continuing through + // to the next observer (or bailing if there's a problem). + __internalEmitPaymentProcessingEvent( + currentObservers.current, + setValidationErrors + ); } }, [ checkoutIsProcessing, checkoutHasError, checkoutIsCalculating, - isPaymentFinished, __internalSetPaymentProcessing, + __internalEmitPaymentProcessingEvent, + setValidationErrors, ] ); - // When checkout is returned to idle, set payment status to pristine but only if payment status is already not finished. + // When checkout is returned to idle, and the payment setup has not completed, set payment status to idle useEffect( () => { - if ( checkoutIsIdle && ! isPaymentSuccess ) { - __internalSetPaymentPristine(); + if ( checkoutIsIdle && ! isPaymentReady ) { + __internalSetPaymentIdle(); } - }, [ checkoutIsIdle, isPaymentSuccess, __internalSetPaymentPristine ] ); + }, [ checkoutIsIdle, isPaymentReady, __internalSetPaymentIdle ] ); - // if checkout has an error sync payment status back to pristine. + // if checkout has an error sync payment status back to idle. useEffect( () => { - if ( checkoutHasError && isPaymentSuccess ) { - __internalSetPaymentPristine(); + if ( checkoutHasError && isPaymentReady ) { + __internalSetPaymentIdle(); } - }, [ checkoutHasError, isPaymentSuccess, __internalSetPaymentPristine ] ); - - // Emit the payment processing event - useEffect( () => { - // Note: the nature of this event emitter is that it will bail on any - // observer that returns a response that !== true. However, this still - // allows for other observers that return true for continuing through - // to the next observer (or bailing if there's a problem). - if ( isPaymentProcessing ) { - __internalEmitPaymentProcessingEvent( - currentObservers.current, - setValidationErrors - ); - } - }, [ - isPaymentProcessing, - setValidationErrors, - __internalEmitPaymentProcessingEvent, - ] ); + }, [ checkoutHasError, isPaymentReady, __internalSetPaymentIdle ] ); const paymentContextData = { onPaymentProcessing, diff --git a/assets/js/base/context/tsconfig.json b/assets/js/base/context/tsconfig.json deleted file mode 100644 index 78b3b4d4549..00000000000 --- a/assets/js/base/context/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../../tsconfig.base.json", - "compilerOptions": {}, - "include": [ - ".", - "../../blocks-registry/index.js", - "../../settings/shared/index.ts", - "../../settings/blocks/index.ts", - "../../base/hooks/index.js", - "../../base/utils/", - "../../utils", - "../../data/", - "../../types/", - "../components", - "../../blocks/cart-checkout-shared/payment-methods", - "../../settings/shared/default-address-fields.ts" - ], - "exclude": [ "**/test/**" ] -} diff --git a/assets/js/blocks/breadcrumbs/style.scss b/assets/js/blocks/breadcrumbs/style.scss index a455e9008dc..bc01b5b8b10 100644 --- a/assets/js/blocks/breadcrumbs/style.scss +++ b/assets/js/blocks/breadcrumbs/style.scss @@ -1,3 +1,9 @@ .woocommerce.wc-block-breadcrumbs { font-size: inherit; } +.woocommerce.woocommerce-shop .wc-block-breadcrumbs { + .woocommerce-breadcrumb { + margin: auto; + display: block; + } +} diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js index c8250967220..c30fa050be1 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js @@ -36,8 +36,8 @@ const ExpressPaymentMethods = () => { ); const { __internalSetActivePaymentMethod, - __internalSetPaymentStarted, - __internalSetPaymentPristine, + __internalSetExpressPaymentStarted, + __internalSetPaymentIdle, __internalSetPaymentError, __internalSetPaymentMethodData, __internalSetExpressPaymentError, @@ -58,14 +58,14 @@ const ExpressPaymentMethods = () => { ( paymentMethodId ) => () => { previousActivePaymentMethod.current = activePaymentMethod; previousPaymentMethodData.current = paymentMethodData; - __internalSetPaymentStarted(); + __internalSetExpressPaymentStarted(); __internalSetActivePaymentMethod( paymentMethodId ); }, [ activePaymentMethod, paymentMethodData, __internalSetActivePaymentMethod, - __internalSetPaymentStarted, + __internalSetExpressPaymentStarted, ] ); @@ -75,12 +75,12 @@ const ExpressPaymentMethods = () => { * This restores the active method and returns the state to pristine. */ const onExpressPaymentClose = useCallback( () => { - __internalSetPaymentPristine(); + __internalSetPaymentIdle(); __internalSetActivePaymentMethod( previousActivePaymentMethod.current, previousPaymentMethodData.current ); - }, [ __internalSetActivePaymentMethod, __internalSetPaymentPristine ] ); + }, [ __internalSetActivePaymentMethod, __internalSetPaymentIdle ] ); /** * onExpressPaymentError should be triggered when the express payment process errors. diff --git a/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx b/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx index 2ac0d9a33c6..ba67ed5d74e 100644 --- a/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx +++ b/assets/js/blocks/cart/cart-cross-sells-product-list/cart-cross-sells-product.tsx @@ -49,7 +49,7 @@ const CartCrossSellsProduct = ( { /> diff --git a/assets/js/blocks/cart/cart-line-items-table/cart-line-item-row.tsx b/assets/js/blocks/cart/cart-line-items-table/cart-line-item-row.tsx index 5ecfb80d399..fdf3976bdcf 100644 --- a/assets/js/blocks/cart/cart-line-items-table/cart-line-item-row.tsx +++ b/assets/js/blocks/cart/cart-line-items-table/cart-line-item-row.tsx @@ -20,10 +20,7 @@ import { ProductSaleBadge, } from '@woocommerce/base-components/cart-checkout'; import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; -import { - __experimentalApplyCheckoutFilter, - mustContain, -} from '@woocommerce/blocks-checkout'; +import { applyCheckoutFilter, mustContain } from '@woocommerce/blocks-checkout'; import Dinero from 'dinero.js'; import { forwardRef, useMemo } from '@wordpress/element'; import type { CartItem } from '@woocommerce/types'; @@ -118,7 +115,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< useStoreCartItemQuantity( lineItem ); const { dispatchStoreEvent } = useStoreEvents(); - // Prepare props to pass to the __experimentalApplyCheckoutFilter filter. + // Prepare props to pass to the applyCheckoutFilter filter. // We need to pluck out receiveCart. // eslint-disable-next-line no-unused-vars const { receiveCart, ...cart } = useStoreCart(); @@ -131,7 +128,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< [ lineItem, cart ] ); const priceCurrency = getCurrencyFromPriceResponse( prices ); - const name = __experimentalApplyCheckoutFilter( { + const name = applyCheckoutFilter( { filterName: 'itemName', defaultValue: initialName, extensions, @@ -163,7 +160,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< const isProductHiddenFromCatalog = catalogVisibility === 'hidden' || catalogVisibility === 'search'; - const cartItemClassNameFilter = __experimentalApplyCheckoutFilter( { + const cartItemClassNameFilter = applyCheckoutFilter( { filterName: 'cartItemClass', defaultValue: '', extensions, @@ -171,7 +168,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< } ); // Allow extensions to filter how the price is displayed. Ie: prepending or appending some values. - const productPriceFormat = __experimentalApplyCheckoutFilter( { + const productPriceFormat = applyCheckoutFilter( { filterName: 'cartItemPrice', defaultValue: '', extensions, @@ -179,7 +176,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< validation: productPriceValidation, } ); - const subtotalPriceFormat = __experimentalApplyCheckoutFilter( { + const subtotalPriceFormat = applyCheckoutFilter( { filterName: 'subtotalPriceFormat', defaultValue: '', extensions, @@ -187,7 +184,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< validation: productPriceValidation, } ); - const saleBadgePriceFormat = __experimentalApplyCheckoutFilter( { + const saleBadgePriceFormat = applyCheckoutFilter( { filterName: 'saleBadgePriceFormat', defaultValue: '', extensions, @@ -195,7 +192,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent< validation: productPriceValidation, } ); - const showRemoveItemLink = __experimentalApplyCheckoutFilter( { + const showRemoveItemLink = applyCheckoutFilter( { filterName: 'showRemoveItemLink', defaultValue: true, extensions, diff --git a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx index 25d8a8a9b20..67c15660241 100644 --- a/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx +++ b/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx @@ -17,9 +17,9 @@ export const Edit = (): JSX.Element => { 'You may be interested in…', 'woo-gutenberg-products-block' ), - level: 3, + level: 2, + fontSize: 'large', }, - , [], ], [ 'woocommerce/cart-cross-sells-products-block', {}, [] ], diff --git a/assets/js/blocks/cart/inner-blocks/cart-order-summary-heading/edit.tsx b/assets/js/blocks/cart/inner-blocks/cart-order-summary-heading/edit.tsx index 2b9c7e08b6a..ee105b65b6d 100644 --- a/assets/js/blocks/cart/inner-blocks/cart-order-summary-heading/edit.tsx +++ b/assets/js/blocks/cart/inner-blocks/cart-order-summary-heading/edit.tsx @@ -37,6 +37,7 @@ export const Edit = ( { onChange={ ( value ) => setAttributes( { content: value } ) } + style={ { backgroundColor: 'transparent' } } />
diff --git a/assets/js/blocks/cart/inner-blocks/empty-cart-block/edit.tsx b/assets/js/blocks/cart/inner-blocks/empty-cart-block/edit.tsx index 7b7a909b23c..d55c95784ea 100644 --- a/assets/js/blocks/cart/inner-blocks/empty-cart-block/edit.tsx +++ b/assets/js/blocks/cart/inner-blocks/empty-cart-block/edit.tsx @@ -15,7 +15,6 @@ import { useForcedLayout, getAllowedBlocks, } from '../../../cart-checkout-shared'; -import iconDataUri from './icon-data-uri.js'; import './style.scss'; const browseStoreTemplate = SHOP_URL @@ -26,7 +25,7 @@ const browseStoreTemplate = SHOP_URL content: sprintf( /* translators: %s is the link to the store product directory. */ __( - 'Browse store.', + 'Browse store', 'woo-gutenberg-products-block' ), SHOP_URL @@ -37,14 +36,6 @@ const browseStoreTemplate = SHOP_URL : null; const defaultTemplate = [ - [ - 'core/image', - { - align: 'center', - url: iconDataUri, - sizeSlug: 'small', - }, - ], [ 'core/heading', { @@ -54,7 +45,7 @@ const defaultTemplate = [ 'woo-gutenberg-products-block' ), level: 2, - className: 'wc-block-cart__empty-cart__title', + className: 'with-empty-cart-icon wc-block-cart__empty-cart__title', }, ], browseStoreTemplate, diff --git a/assets/js/blocks/cart/inner-blocks/empty-cart-block/icon-data-uri.js b/assets/js/blocks/cart/inner-blocks/empty-cart-block/icon-data-uri.js deleted file mode 100644 index d5082ec4a4e..00000000000 --- a/assets/js/blocks/cart/inner-blocks/empty-cart-block/icon-data-uri.js +++ /dev/null @@ -1 +0,0 @@ -export default 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzgiIGhlaWdodD0iMzgiIHZpZXdCb3g9IjAgMCAzOCAzOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDBDOC41MDQwMyAwIDAgOC41MDQwMyAwIDE5QzAgMjkuNDk2IDguNTA0MDMgMzggMTkgMzhDMjkuNDk2IDM4IDM4IDI5LjQ5NiAzOCAxOUMzOCA4LjUwNDAzIDI5LjQ5NiAwIDE5IDBaTTI1LjEyOSAxMi44NzFDMjYuNDg1MSAxMi44NzEgMjcuNTgwNiAxMy45NjY1IDI3LjU4MDYgMTUuMzIyNkMyNy41ODA2IDE2LjY3ODYgMjYuNDg1MSAxNy43NzQyIDI1LjEyOSAxNy43NzQyQzIzLjc3MyAxNy43NzQyIDIyLjY3NzQgMTYuNjc4NiAyMi42Nzc0IDE1LjMyMjZDMjIuNjc3NCAxMy45NjY1IDIzLjc3MyAxMi44NzEgMjUuMTI5IDEyLjg3MVpNMTEuNjQ1MiAzMS4yNTgxQzkuNjE0OTIgMzEuMjU4MSA3Ljk2Nzc0IDI5LjY0OTIgNy45Njc3NCAyNy42NTczQzcuOTY3NzQgMjYuMTI1IDEwLjE1MTIgMjMuMDI5OCAxMS4xNTQ4IDIxLjY5NjhDMTEuNCAyMS4zNjczIDExLjg5MDMgMjEuMzY3MyAxMi4xMzU1IDIxLjY5NjhDMTMuMTM5MSAyMy4wMjk4IDE1LjMyMjYgMjYuMTI1IDE1LjMyMjYgMjcuNjU3M0MxNS4zMjI2IDI5LjY0OTIgMTMuNjc1NCAzMS4yNTgxIDExLjY0NTIgMzEuMjU4MVpNMTIuODcxIDE3Ljc3NDJDMTEuNTE0OSAxNy43NzQyIDEwLjQxOTQgMTYuNjc4NiAxMC40MTk0IDE1LjMyMjZDMTAuNDE5NCAxMy45NjY1IDExLjUxNDkgMTIuODcxIDEyLjg3MSAxMi44NzFDMTQuMjI3IDEyLjg3MSAxNS4zMjI2IDEzLjk2NjUgMTUuMzIyNiAxNS4zMjI2QzE1LjMyMjYgMTYuNjc4NiAxNC4yMjcgMTcuNzc0MiAxMi44NzEgMTcuNzc0MlpNMjUuOTEwNSAyOS41ODc5QzI0LjE5NDQgMjcuNTM0NyAyMS42NzM4IDI2LjM1NDggMTkgMjYuMzU0OEMxNy4zNzU4IDI2LjM1NDggMTcuMzc1OCAyMy45MDMyIDE5IDIzLjkwMzJDMjIuNDAxNiAyMy45MDMyIDI1LjYxMTcgMjUuNDA0OCAyNy43ODc1IDI4LjAyNUMyOC44NDQ4IDI5LjI4MTUgMjYuOTI5NCAzMC44MjE0IDI1LjkxMDUgMjkuNTg3OVoiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo='; diff --git a/assets/js/blocks/cart/inner-blocks/empty-cart-block/style.scss b/assets/js/blocks/cart/inner-blocks/empty-cart-block/style.scss index e0c402f6ee1..1c96b67ef01 100644 --- a/assets/js/blocks/cart/inner-blocks/empty-cart-block/style.scss +++ b/assets/js/blocks/cart/inner-blocks/empty-cart-block/style.scss @@ -2,3 +2,21 @@ .editor-styles-wrapper .wc-block-cart__empty-cart__title { font-size: inherit; } +.wc-block-cart__empty-cart__title.with-empty-cart-icon { + &::before { + content: ""; + background-color: currentColor; + display: block; + margin: 0 auto 2em; + mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMTkgMEM4LjUwNCAwIDAgOC41MDQgMCAxOXM4LjUwNCAxOSAxOSAxOSAxOS04LjUwNCAxOS0xOVMyOS40OTYgMCAxOSAwWm02LjEyOSAxMi44NzFhMi40NDkgMi40NDkgMCAwIDEgMi40NTIgMi40NTIgMi40NDkgMi40NDkgMCAwIDEtMi40NTIgMi40NTEgMi40NDkgMi40NDkgMCAwIDEtMi40NTItMi40NTEgMi40NDkgMi40NDkgMCAwIDEgMi40NTItMi40NTJaTTExLjY0NSAzMS4yNThjLTIuMDMgMC0zLjY3Ny0xLjYwOS0zLjY3Ny0zLjYgMC0xLjUzMyAyLjE4My00LjYyOCAzLjE4Ny01Ljk2MWEuNjEuNjEgMCAwIDEgLjk4IDBjMS4wMDQgMS4zMzMgMy4xODggNC40MjggMy4xODggNS45NiAwIDEuOTkyLTEuNjQ4IDMuNjAxLTMuNjc4IDMuNjAxWm0xLjIyNi0xMy40ODRhMi40NDkgMi40NDkgMCAwIDEtMi40NTItMi40NTEgMi40NDkgMi40NDkgMCAwIDEgMi40NTItMi40NTIgMi40NDkgMi40NDkgMCAwIDEgMi40NTIgMi40NTIgMi40NDkgMi40NDkgMCAwIDEtMi40NTIgMi40NTFabTEzLjA0IDExLjgxNEE4Ljk4OSA4Ljk4OSAwIDAgMCAxOSAyNi4zNTVjLTEuNjI0IDAtMS42MjQtMi40NTIgMC0yLjQ1MiAzLjQwMiAwIDYuNjEyIDEuNTAyIDguNzg4IDQuMTIyIDEuMDU3IDEuMjU3LS44NTkgMi43OTYtMS44NzggMS41NjNaIi8+PC9zdmc+); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 5em; + width: 5em; + height: 5em; + } +} +.wp-block-woocommerce-empty-cart-block > .aligncenter { + margin-left: auto !important; + margin-right: auto !important; +} diff --git a/assets/js/blocks/cart/test/block.js b/assets/js/blocks/cart/test/block.js index 14ba47dc156..3eea7abf2fa 100644 --- a/assets/js/blocks/cart/test/block.js +++ b/assets/js/blocks/cart/test/block.js @@ -6,7 +6,7 @@ import { previewCart } from '@woocommerce/resource-previews'; import { dispatch } from '@wordpress/data'; import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; import { default as fetchMock } from 'jest-fetch-mock'; -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; +import { registerCheckoutFilters } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -240,7 +240,7 @@ describe( 'Testing cart', () => { items: [ previewCart.items[ 0 ] ], }; - __experimentalRegisterCheckoutFilters( 'woo-blocks-test-extension', { + registerCheckoutFilters( 'woo-blocks-test-extension', { showRemoveItemLink: ( value, extensions, { cartItem } ) => { return cartItem.id !== cart.items[ 0 ].id; }, diff --git a/assets/js/blocks/catalog-sorting/style.scss b/assets/js/blocks/catalog-sorting/style.scss index 85ecc5acf2f..5be096ed2ab 100644 --- a/assets/js/blocks/catalog-sorting/style.scss +++ b/assets/js/blocks/catalog-sorting/style.scss @@ -7,4 +7,8 @@ font-size: inherit; color: inherit; } + + .woocommerce-ordering { + margin: auto; + } } diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 1cce8fc71d0..18d1a0ac174 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -183,8 +183,9 @@ const Block = ( { ) } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } diff --git a/assets/js/blocks/checkout/form-step/form-step-block.tsx b/assets/js/blocks/checkout/form-step/form-step-block.tsx index d4eaee87de3..d20d806df96 100644 --- a/assets/js/blocks/checkout/form-step/form-step-block.tsx +++ b/assets/js/blocks/checkout/form-step/form-step-block.tsx @@ -66,6 +66,7 @@ export const FormStepBlock = ( { className={ '' } value={ title } onChange={ ( value ) => setAttributes( { title: value } ) } + style={ { backgroundColor: 'transparent' } } />
@@ -86,6 +87,7 @@ export const FormStepBlock = ( { description: value, } ) } + style={ { backgroundColor: 'transparent' } } />

diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx index 4ad6356032a..b4c36a071fb 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx @@ -11,7 +11,7 @@ import { useCheckoutSubmit } from '@woocommerce/base-context/hooks'; import { noticeContexts } from '@woocommerce/base-context'; import { StoreNoticesContainer, - __experimentalApplyCheckoutFilter, + applyCheckoutFilter, } from '@woocommerce/blocks-checkout'; /** @@ -32,7 +32,7 @@ const Block = ( { placeOrderButtonLabel: string; } ): JSX.Element => { const { paymentMethodButtonLabel } = useCheckoutSubmit(); - const label = __experimentalApplyCheckoutFilter( { + const label = applyCheckoutFilter( { filterName: 'placeOrderButtonLabel', defaultValue: paymentMethodButtonLabel || diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx index 3d42b8ebc6a..1d372046da0 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx @@ -88,10 +88,13 @@ const Block = ( { ] ) as Record< keyof AddressFields, Partial< AddressField > >; const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment; + const noticeContext = useBillingAsShipping + ? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ] + : [ noticeContexts.BILLING_ADDRESS ]; return ( - + { setUseStoreCartReturnValue(); const { container } = render( ); expect( - await findByText( container, 'Apply a coupon code' ) + await findByText( container, 'Add a coupon' ) ).toBeInTheDocument(); } ); diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 891e962ebc9..e1fdfa98f02 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -97,13 +97,14 @@ const Block = ( { ] ) as Record< keyof AddressFields, Partial< AddressField > >; const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment; + const noticeContext = useShippingAsBilling + ? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ] + : [ noticeContexts.SHIPPING_ADDRESS ]; return ( <> - + { ) } - { - // Hacky temporary solution to display the feedback prompt - // at the bottom of the inspector controls - } - - - ); }; @@ -226,6 +219,9 @@ export const withProductQueryControls = <> + + + ) : ( diff --git a/assets/js/blocks/product-query/variations/product-query.tsx b/assets/js/blocks/product-query/variations/product-query.tsx index 73362b8bf0d..b5804d87d17 100644 --- a/assets/js/blocks/product-query/variations/product-query.tsx +++ b/assets/js/blocks/product-query/variations/product-query.tsx @@ -1,11 +1,16 @@ /** * External dependencies */ -import { registerBlockVariation } from '@wordpress/blocks'; +import { + registerBlockVariation, + unregisterBlockVariation, +} from '@wordpress/blocks'; import { Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { stacks } from '@woocommerce/icons'; import { isWpVersion } from '@woocommerce/settings'; +import { select, subscribe } from '@wordpress/data'; +import { QueryBlockAttributes } from '@woocommerce/blocks/product-query/types'; /** * Internal dependencies @@ -19,7 +24,15 @@ import { const VARIATION_NAME = 'woocommerce/product-query'; -if ( isWpVersion( '6.1', '>=' ) ) { +const ARCHIVE_PRODUCT_TEMPLATES = [ + 'woocommerce/woocommerce//archive-product', + 'woocommerce/woocommerce//taxonomy-product_cat', + 'woocommerce/woocommerce//taxonomy-product_tag', + 'woocommerce/woocommerce//taxonomy-product_attribute', + 'woocommerce/woocommerce//product-search-results', +]; + +const registerProductsBlock = ( attributes: QueryBlockAttributes ) => { registerBlockVariation( QUERY_LOOP_ID, { description: __( 'A block that displays a selection of products in your store.', @@ -37,7 +50,7 @@ if ( isWpVersion( '6.1', '>=' ) ) { /> ), attributes: { - ...QUERY_DEFAULT_ATTRIBUTES, + ...attributes, namespace: VARIATION_NAME, }, // Gutenberg doesn't support this type yet, discussion here: @@ -48,4 +61,37 @@ if ( isWpVersion( '6.1', '>=' ) ) { innerBlocks: INNER_BLOCKS_TEMPLATE, scope: [ 'inserter' ], } ); +}; + +if ( isWpVersion( '6.1', '>=' ) ) { + const store = select( 'core/edit-site' ); + + if ( store ) { + let currentTemplateId: string | undefined; + + subscribe( () => { + const previousTemplateId = currentTemplateId; + + currentTemplateId = store?.getEditedPostId(); + + if ( previousTemplateId === currentTemplateId ) { + return; + } + + const queryAttributes = { + ...QUERY_DEFAULT_ATTRIBUTES, + query: { + ...QUERY_DEFAULT_ATTRIBUTES.query, + inherit: + ARCHIVE_PRODUCT_TEMPLATES.includes( currentTemplateId ), + }, + }; + + unregisterBlockVariation( QUERY_LOOP_ID, VARIATION_NAME ); + + registerProductsBlock( queryAttributes ); + } ); + } else { + registerProductsBlock( QUERY_DEFAULT_ATTRIBUTES ); + } } diff --git a/assets/js/blocks/product-results-count/style.scss b/assets/js/blocks/product-results-count/style.scss index 5ed82602ca6..54c9cfe3de8 100644 --- a/assets/js/blocks/product-results-count/style.scss +++ b/assets/js/blocks/product-results-count/style.scss @@ -5,5 +5,8 @@ .woocommerce-result-count { float: none; font-size: inherit; + // reset for margin + margin: auto; } } + diff --git a/assets/js/blocks/product-search/edit.js b/assets/js/blocks/product-search/edit.js index c881d261b50..74d0530e02a 100644 --- a/assets/js/blocks/product-search/edit.js +++ b/assets/js/blocks/product-search/edit.js @@ -86,6 +86,7 @@ const Edit = ( { onChange={ ( value ) => setAttributes( { label: value } ) } + style={ { backgroundColor: 'transparent' } } /> ) } diff --git a/assets/js/data/cart/push-changes.ts b/assets/js/data/cart/push-changes.ts index 4484b978e29..06228265a8e 100644 --- a/assets/js/data/cart/push-changes.ts +++ b/assets/js/data/cart/push-changes.ts @@ -3,11 +3,7 @@ */ import { debounce, pick } from 'lodash'; import { select, dispatch } from '@wordpress/data'; -import { - pluckAddress, - pluckEmail, - removeAllNotices, -} from '@woocommerce/base-utils'; +import { pluckEmail, removeAllNotices } from '@woocommerce/base-utils'; import { CartBillingAddress, CartShippingAddress, @@ -27,15 +23,33 @@ type CustomerData = { shippingAddress: CartShippingAddress; }; +type BillingOrShippingAddress = CartBillingAddress | CartShippingAddress; + /** * Checks if a cart response contains an email property. */ const isBillingAddress = ( - address: CartBillingAddress | CartShippingAddress + address: BillingOrShippingAddress ): address is CartBillingAddress => { return 'email' in address; }; +export const trimAddress = ( address: BillingOrShippingAddress ) => { + const trimmedAddress = { + ...address, + }; + Object.keys( address ).forEach( ( key ) => { + trimmedAddress[ key as keyof BillingOrShippingAddress ] = + address[ key as keyof BillingOrShippingAddress ].trim(); + } ); + + trimmedAddress.postcode = trimmedAddress.postcode + ? trimmedAddress.postcode.replace( ' ', '' ).toUpperCase() + : ''; + + return trimmedAddress; +}; + /** * Does a shallow compare of important address data to determine if the cart needs updating on the server. This takes * the current and previous address into account, as well as the billing email field. @@ -57,8 +71,8 @@ const isAddressDirty = < T extends CartBillingAddress | CartShippingAddress >( return ( !! address.country && ! isShallowEqual( - pluckAddress( previousAddress ), - pluckAddress( address ) + trimAddress( previousAddress ), + trimAddress( address ) ) ); }; diff --git a/assets/js/data/checkout/constants.ts b/assets/js/data/checkout/constants.ts index 31bd2b71981..a5b16a1ca82 100644 --- a/assets/js/data/checkout/constants.ts +++ b/assets/js/data/checkout/constants.ts @@ -12,8 +12,6 @@ import { CheckoutResponseSuccess } from '@woocommerce/types'; export const STORE_KEY = 'wc/store/checkout'; export enum STATUS { - // Checkout is in its initialized state. - PRISTINE = 'pristine', // When checkout state has changed but there is no activity happening. IDLE = 'idle', // After the AFTER_PROCESSING event emitters have completed. This status triggers the checkout redirect. diff --git a/assets/js/data/checkout/reducers.ts b/assets/js/data/checkout/reducers.ts index 93cd3c02d6e..fa7845715a3 100644 --- a/assets/js/data/checkout/reducers.ts +++ b/assets/js/data/checkout/reducers.ts @@ -166,14 +166,6 @@ const reducer = ( state = defaultState, action: CheckoutAction ) => { } break; } - - if ( - newState !== state && - action.type !== types.SET_PRISTINE && - newState?.status === STATUS.PRISTINE - ) { - newState.status = STATUS.IDLE; - } return newState; }; diff --git a/assets/js/data/checkout/test/reducer.ts b/assets/js/data/checkout/test/reducer.ts index 4df3e787ef7..f83b5985a99 100644 --- a/assets/js/data/checkout/test/reducer.ts +++ b/assets/js/data/checkout/test/reducer.ts @@ -26,7 +26,6 @@ describe.only( 'Checkout Store Reducer', () => { const expectedState = { ...defaultState, redirectUrl: 'https://example.com', - status: STATUS.IDLE, }; expect( @@ -97,12 +96,15 @@ describe.only( 'Checkout Store Reducer', () => { } ); it( 'should handle SET_HAS_ERROR when status is anything else', () => { - const initialState = { ...defaultState, status: STATUS.PRISTINE }; + const initialState = { + ...defaultState, + status: STATUS.AFTER_PROCESSING, + }; const expectedState = { ...defaultState, hasError: false, - status: STATUS.IDLE, + status: STATUS.AFTER_PROCESSING, }; expect( @@ -135,7 +137,6 @@ describe.only( 'Checkout Store Reducer', () => { it( 'should handle INCREMENT_CALCULATING', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, calculatingCount: 1, }; @@ -152,7 +153,6 @@ describe.only( 'Checkout Store Reducer', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, calculatingCount: 0, }; @@ -164,7 +164,6 @@ describe.only( 'Checkout Store Reducer', () => { it( 'should handle SET_CUSTOMER_ID', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, customerId: 1, }; @@ -176,7 +175,6 @@ describe.only( 'Checkout Store Reducer', () => { it( 'should handle SET_USE_SHIPPING_AS_BILLING', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, useShippingAsBilling: false, }; @@ -191,7 +189,6 @@ describe.only( 'Checkout Store Reducer', () => { it( 'should handle SET_SHOULD_CREATE_ACCOUNT', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, shouldCreateAccount: true, }; @@ -206,7 +203,6 @@ describe.only( 'Checkout Store Reducer', () => { it( 'should handle SET_ORDER_NOTES', () => { const expectedState = { ...defaultState, - status: STATUS.IDLE, orderNotes: 'test', }; @@ -225,7 +221,6 @@ describe.only( 'Checkout Store Reducer', () => { }; const expectedState = { ...defaultState, - status: STATUS.IDLE, extensionData: mockExtensionData, }; expect( @@ -247,7 +242,6 @@ describe.only( 'Checkout Store Reducer', () => { }; const expectedState = { ...defaultState, - status: STATUS.IDLE, extensionData: mockExtensionData, }; const firstState = reducer( @@ -272,7 +266,6 @@ describe.only( 'Checkout Store Reducer', () => { }; const expectedState = { ...defaultState, - status: STATUS.IDLE, extensionData: mockExtensionData, }; const firstState = reducer( diff --git a/assets/js/data/checkout/types.ts b/assets/js/data/checkout/types.ts index 6057407e1da..acec22130c6 100644 --- a/assets/js/data/checkout/types.ts +++ b/assets/js/data/checkout/types.ts @@ -3,6 +3,7 @@ */ import type { Notice } from '@wordpress/notices/'; import { DataRegistry } from '@wordpress/data'; +import { FieldValidationStatus } from '@woocommerce/types'; /** * Internal dependencies @@ -13,7 +14,6 @@ import type { PaymentState } from '../payment/default-state'; import type { DispatchFromMap, SelectFromMap } from '../mapped-types'; import * as selectors from './selectors'; import * as actions from './actions'; -import { FieldValidationStatus } from '../types'; export type CheckoutAfterProcessingWithErrorEventData = { redirectUrl: CheckoutState[ 'redirectUrl' ]; diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts index 5367e06bb06..4cd685a5c09 100644 --- a/assets/js/data/index.ts +++ b/assets/js/data/index.ts @@ -15,5 +15,4 @@ export { VALIDATION_STORE_KEY } from './validation'; export { QUERY_STATE_STORE_KEY } from './query-state'; export { STORE_NOTICES_STORE_KEY } from './store-notices'; export * from './constants'; -export * from './types'; export * from './utils'; diff --git a/assets/js/data/payment/action-types.ts b/assets/js/data/payment/action-types.ts index f73208e3e83..5e4a57613a0 100644 --- a/assets/js/data/payment/action-types.ts +++ b/assets/js/data/payment/action-types.ts @@ -1,10 +1,9 @@ export enum ACTION_TYPES { - SET_PAYMENT_PRISTINE = 'SET_PAYMENT_PRISTINE', - SET_PAYMENT_STARTED = 'SET_PAYMENT_STARTED', + SET_PAYMENT_IDLE = 'SET_PAYMENT_IDLE', + SET_EXPRESS_PAYMENT_STARTED = 'SET_EXPRESS_PAYMENT_STARTED', + SET_PAYMENT_READY = 'SET_PAYMENT_READY', SET_PAYMENT_PROCESSING = 'SET_PAYMENT_PROCESSING', - SET_PAYMENT_FAILED = 'SET_PAYMENT_FAILED', SET_PAYMENT_ERROR = 'SET_PAYMENT_ERROR', - SET_PAYMENT_SUCCESS = 'SET_PAYMENT_SUCCESS', SET_PAYMENT_METHODS_INITIALIZED = 'SET_PAYMENT_METHODS_INITIALIZED', SET_EXPRESS_PAYMENT_METHODS_INITIALIZED = 'SET_EXPRESS_PAYMENT_METHODS_INITIALIZED', SET_ACTIVE_PAYMENT_METHOD = 'SET_ACTIVE_PAYMENT_METHOD', diff --git a/assets/js/data/payment/actions.ts b/assets/js/data/payment/actions.ts index 8ee3d412fee..f1c735111a5 100644 --- a/assets/js/data/payment/actions.ts +++ b/assets/js/data/payment/actions.ts @@ -17,28 +17,24 @@ import { setDefaultPaymentMethod } from './utils/set-default-payment-method'; // `Thunks are functions that can be dispatched, similar to actions creators export * from './thunks'; -export const __internalSetPaymentPristine = () => ( { - type: ACTION_TYPES.SET_PAYMENT_PRISTINE, +export const __internalSetPaymentIdle = () => ( { + type: ACTION_TYPES.SET_PAYMENT_IDLE, } ); -export const __internalSetPaymentStarted = () => ( { - type: ACTION_TYPES.SET_PAYMENT_STARTED, +export const __internalSetExpressPaymentStarted = () => ( { + type: ACTION_TYPES.SET_EXPRESS_PAYMENT_STARTED, } ); export const __internalSetPaymentProcessing = () => ( { type: ACTION_TYPES.SET_PAYMENT_PROCESSING, } ); -export const __internalSetPaymentFailed = () => ( { - type: ACTION_TYPES.SET_PAYMENT_FAILED, -} ); - export const __internalSetPaymentError = () => ( { type: ACTION_TYPES.SET_PAYMENT_ERROR, } ); -export const __internalSetPaymentSuccess = () => ( { - type: ACTION_TYPES.SET_PAYMENT_SUCCESS, +export const __internalSetPaymentReady = () => ( { + type: ACTION_TYPES.SET_PAYMENT_READY, } ); /** diff --git a/assets/js/data/payment/constants.ts b/assets/js/data/payment/constants.ts index f77e56d5aee..9f4ccd0f72b 100644 --- a/assets/js/data/payment/constants.ts +++ b/assets/js/data/payment/constants.ts @@ -1,10 +1,9 @@ export const STORE_KEY = 'wc/store/payment'; export enum STATUS { - PRISTINE = 'pristine', - STARTED = 'started', + IDLE = 'idle', + EXPRESS_STARTED = 'express_started', PROCESSING = 'processing', + READY = 'ready', ERROR = 'has_error', - FAILED = 'failed', - SUCCESS = 'success', } diff --git a/assets/js/data/payment/default-state.ts b/assets/js/data/payment/default-state.ts index 660951c0b04..21e121cb61a 100644 --- a/assets/js/data/payment/default-state.ts +++ b/assets/js/data/payment/default-state.ts @@ -32,7 +32,7 @@ export interface PaymentState { } export const defaultPaymentState: PaymentState = { - status: PAYMENT_STATUS.PRISTINE, + status: PAYMENT_STATUS.IDLE, activePaymentMethod: '', activeSavedToken: '', availablePaymentMethods: {}, diff --git a/assets/js/data/payment/reducers.ts b/assets/js/data/payment/reducers.ts index 18723fb3785..800db3f45fb 100644 --- a/assets/js/data/payment/reducers.ts +++ b/assets/js/data/payment/reducers.ts @@ -17,17 +17,17 @@ const reducer: Reducer< PaymentState > = ( ) => { let newState = state; switch ( action.type ) { - case ACTION_TYPES.SET_PAYMENT_PRISTINE: + case ACTION_TYPES.SET_PAYMENT_IDLE: newState = { ...state, - status: STATUS.PRISTINE, + status: STATUS.IDLE, }; break; - case ACTION_TYPES.SET_PAYMENT_STARTED: + case ACTION_TYPES.SET_EXPRESS_PAYMENT_STARTED: newState = { ...state, - status: STATUS.STARTED, + status: STATUS.EXPRESS_STARTED, }; break; @@ -38,10 +38,10 @@ const reducer: Reducer< PaymentState > = ( }; break; - case ACTION_TYPES.SET_PAYMENT_FAILED: + case ACTION_TYPES.SET_PAYMENT_READY: newState = { ...state, - status: STATUS.FAILED, + status: STATUS.READY, }; break; @@ -52,13 +52,6 @@ const reducer: Reducer< PaymentState > = ( }; break; - case ACTION_TYPES.SET_PAYMENT_SUCCESS: - newState = { - ...state, - status: STATUS.SUCCESS, - }; - break; - case ACTION_TYPES.SET_SHOULD_SAVE_PAYMENT_METHOD: newState = { ...state, diff --git a/assets/js/data/payment/selectors.ts b/assets/js/data/payment/selectors.ts index dd87cd4cdaf..afc815d85ce 100644 --- a/assets/js/data/payment/selectors.ts +++ b/assets/js/data/payment/selectors.ts @@ -14,6 +14,7 @@ import { filterActiveSavedPaymentMethods } from './utils/filter-active-saved-pay import { STATUS as PAYMENT_STATUS } from './constants'; const globalPaymentMethods: Record< string, string > = {}; + if ( getSetting( 'globalPaymentMethods' ) ) { getSetting< GlobalPaymentMethod[] >( 'globalPaymentMethods' ).forEach( ( method ) => { @@ -22,30 +23,62 @@ if ( getSetting( 'globalPaymentMethods' ) ) { ); } -export const isPaymentPristine = ( state: PaymentState ) => - state.status === PAYMENT_STATUS.PRISTINE; +export const isPaymentPristine = ( state: PaymentState ) => { + deprecated( 'isPaymentPristine', { + since: '9.6.0', + alternative: 'isPaymentIdle', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + + return state.status === PAYMENT_STATUS.IDLE; +}; + +export const isPaymentIdle = ( state: PaymentState ) => + state.status === PAYMENT_STATUS.IDLE; -export const isPaymentStarted = ( state: PaymentState ) => - state.status === PAYMENT_STATUS.STARTED; +export const isPaymentStarted = ( state: PaymentState ) => { + deprecated( 'isPaymentStarted', { + since: '9.6.0', + alternative: 'isExpressPaymentStarted', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return state.status === PAYMENT_STATUS.EXPRESS_STARTED; +}; + +export const isExpressPaymentStarted = ( state: PaymentState ) => { + return state.status === PAYMENT_STATUS.EXPRESS_STARTED; +}; export const isPaymentProcessing = ( state: PaymentState ) => state.status === PAYMENT_STATUS.PROCESSING; -export const isPaymentSuccess = ( state: PaymentState ) => - state.status === PAYMENT_STATUS.SUCCESS; +export const isPaymentReady = ( state: PaymentState ) => + state.status === PAYMENT_STATUS.READY; + +export const isPaymentSuccess = ( state: PaymentState ) => { + deprecated( 'isPaymentSuccess', { + since: '9.6.0', + alternative: 'isPaymentReady', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + + return state.status === PAYMENT_STATUS.READY; +}; export const hasPaymentError = ( state: PaymentState ) => state.status === PAYMENT_STATUS.ERROR; -export const isPaymentFailed = ( state: PaymentState ) => - state.status === PAYMENT_STATUS.FAILED; +export const isPaymentFailed = ( state: PaymentState ) => { + deprecated( 'isPaymentFailed', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); -export const isPaymentFinished = ( state: PaymentState ) => { - return ( - state.status === PAYMENT_STATUS.SUCCESS || - state.status === PAYMENT_STATUS.ERROR || - state.status === PAYMENT_STATUS.FAILED - ); + return state.status === PAYMENT_STATUS.ERROR; }; export const isExpressPaymentMethodActive = ( state: PaymentState ) => { @@ -119,26 +152,54 @@ export const expressPaymentMethodsInitialized = ( state: PaymentState ) => { }; /** - * @deprecated - use these selectors instead: isPaymentPristine, isPaymentStarted, isPaymentProcessing, - * isPaymentFinished, hasPaymentError, isPaymentSuccess, isPaymentFailed + * @deprecated - Use these selectors instead: isPaymentIdle, isPaymentProcessing, + * hasPaymentError */ export const getCurrentStatus = ( state: PaymentState ) => { deprecated( 'getCurrentStatus', { since: '8.9.0', - alternative: - 'isPaymentPristine, isPaymentStarted, isPaymentProcessing, isPaymentFinished, hasPaymentError, isPaymentSuccess, isPaymentFailed', + alternative: 'isPaymentIdle, isPaymentProcessing, hasPaymentError', plugin: 'WooCommerce Blocks', link: 'https://github.com/woocommerce/woocommerce-blocks/pull/7666', } ); return { - isPristine: isPaymentPristine( state ), - isStarted: isPaymentStarted( state ), + get isPristine() { + deprecated( 'isPristine', { + since: '9.6.0', + alternative: 'isIdle', + plugin: 'WooCommerce Blocks', + } ); + return isPaymentIdle( state ); + }, // isPristine is the same as isIdle. + isIdle: isPaymentIdle( state ), + isStarted: isExpressPaymentStarted( state ), isProcessing: isPaymentProcessing( state ), - isFinished: isPaymentFinished( state ), + get isFinished() { + deprecated( 'isFinished', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return hasPaymentError( state ) || isPaymentReady( state ); + }, hasError: hasPaymentError( state ), - hasFailed: isPaymentFailed( state ), - isSuccessful: isPaymentSuccess( state ), + get hasFailed() { + deprecated( 'hasFailed', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return hasPaymentError( state ); + }, + get isSuccessful() { + deprecated( 'isSuccessful', { + since: '9.6.0', + plugin: 'WooCommerce Blocks', + link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110', + } ); + return isPaymentReady( state ); + }, isDoingExpressPayment: isExpressPaymentMethodActive( state ), }; }; diff --git a/assets/js/data/payment/test/set-default-payment-method.ts b/assets/js/data/payment/test/set-default-payment-method.ts index c0ca7ae989c..aa1a21e82c2 100644 --- a/assets/js/data/payment/test/set-default-payment-method.ts +++ b/assets/js/data/payment/test/set-default-payment-method.ts @@ -126,11 +126,10 @@ describe( 'setDefaultPaymentMethod', () => { __internalSetActivePaymentMethod: setActivePaymentMethodMock, __internalSetPaymentError: () => void 0, - __internalSetPaymentFailed: () => void 0, - __internalSetPaymentSuccess: () => void 0, - __internalSetPaymentPristine: () => void 0, - __internalSetPaymentStarted: () => void 0, + __internalSetPaymentIdle: () => void 0, + __internalSetExpressPaymentStarted: () => void 0, __internalSetPaymentProcessing: () => void 0, + __internalSetPaymentReady: () => void 0, }; } return originalStore; diff --git a/assets/js/data/payment/test/thunks.tsx b/assets/js/data/payment/test/thunks.tsx index dfe1a755ec8..d3377df4303 100644 --- a/assets/js/data/payment/test/thunks.tsx +++ b/assets/js/data/payment/test/thunks.tsx @@ -191,7 +191,7 @@ describe( 'wc/store/payment thunks', () => { } ); const setPaymentErrorMock = jest.fn(); - const setPaymentSuccessMock = jest.fn(); + const setPaymentReadyMock = jest.fn(); const registryMock = { dispatch: jest .fn() @@ -211,14 +211,14 @@ describe( 'wc/store/payment thunks', () => { dispatch: { ...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ), __internalSetPaymentError: setPaymentErrorMock, - __internalSetPaymentSuccess: setPaymentSuccessMock, + __internalSetPaymentReady: setPaymentReadyMock, }, } ); // The observer throwing will cause this. //expect( console ).toHaveErroredWith( new Error( 'test error' ) ); expect( setPaymentErrorMock ).toHaveBeenCalled(); - expect( setPaymentSuccessMock ).not.toHaveBeenCalled(); + expect( setPaymentReadyMock ).not.toHaveBeenCalled(); } ); } ); } ); diff --git a/assets/js/data/payment/thunks.ts b/assets/js/data/payment/thunks.ts index 1ba23513727..d3fb85ab359 100644 --- a/assets/js/data/payment/thunks.ts +++ b/assets/js/data/payment/thunks.ts @@ -4,6 +4,7 @@ import { store as noticesStore } from '@wordpress/notices'; import deprecated from '@wordpress/deprecated'; import type { BillingAddress, ShippingAddress } from '@woocommerce/settings'; +import { isObject, isString, objectHasProp } from '@woocommerce/types'; /** * Internal dependencies @@ -14,6 +15,7 @@ import { isFailResponse, isSuccessResponse, noticeContexts, + ObserverResponse, } from '../../base/context/event-emit'; import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit'; import type { emitProcessingEventType } from './types'; @@ -22,6 +24,8 @@ import { isBillingAddress, isShippingAddress, } from '../../types/type-guards/address'; +import { isObserverResponse } from '../../types/type-guards/observers'; +import { isValidValidationErrorsObject } from '../../types/type-guards/validation'; export const __internalSetExpressPaymentError = ( message?: string ) => { return ( { registry } ) => { @@ -57,15 +61,17 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = ( EMIT_TYPES.PAYMENT_PROCESSING, {} ).then( ( observerResponses ) => { - let successResponse, - errorResponse, + let successResponse: ObserverResponse | undefined, + errorResponse: ObserverResponse | undefined, billingAddress: BillingAddress | undefined, shippingAddress: ShippingAddress | undefined; observerResponses.forEach( ( response ) => { if ( isSuccessResponse( response ) ) { - // the last observer response always "wins" for success. + // The last observer response always "wins" for success. successResponse = response; } + + // We consider both failed and error responses as an error. if ( isErrorResponse( response ) || isFailResponse( response ) @@ -86,12 +92,13 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = ( shippingData: shippingDataFromResponse, } = response?.meta || {}; - billingAddress = billingAddressFromResponse; - shippingAddress = shippingAddressFromResponse; + billingAddress = billingAddressFromResponse as BillingAddress; + shippingAddress = + shippingAddressFromResponse as ShippingAddress; if ( billingDataFromResponse ) { // Set this here so that old extensions still using billingData can set the billingAddress. - billingAddress = billingDataFromResponse; + billingAddress = billingDataFromResponse as BillingAddress; deprecated( 'returning billingData from an onPaymentProcessing observer in WooCommerce Blocks', { @@ -104,7 +111,8 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = ( if ( shippingDataFromResponse ) { // Set this here so that old extensions still using shippingData can set the shippingAddress. - shippingAddress = shippingDataFromResponse; + shippingAddress = + shippingDataFromResponse as ShippingAddress; deprecated( 'returning shippingData from an onPaymentProcessing observer in WooCommerce Blocks', { @@ -119,54 +127,85 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = ( const { setBillingAddress, setShippingAddress } = registry.dispatch( CART_STORE_KEY ); - if ( successResponse && ! errorResponse ) { + // Observer returned success, we sync the payment method data and billing address. + if ( isObserverResponse( successResponse ) && ! errorResponse ) { const { paymentMethodData } = successResponse?.meta || {}; - if ( billingAddress && isBillingAddress( billingAddress ) ) { + if ( isBillingAddress( billingAddress ) ) { setBillingAddress( billingAddress ); } - if ( - typeof shippingAddress !== 'undefined' && - isShippingAddress( shippingAddress ) - ) { + if ( isShippingAddress( shippingAddress ) ) { setShippingAddress( shippingAddress ); } - dispatch.__internalSetPaymentMethodData( paymentMethodData ); - dispatch.__internalSetPaymentSuccess(); - } else if ( errorResponse && isFailResponse( errorResponse ) ) { - if ( errorResponse.message && errorResponse.message.length ) { + + dispatch.__internalSetPaymentMethodData( + isObject( paymentMethodData ) ? paymentMethodData : {} + ); + dispatch.__internalSetPaymentReady(); + } else if ( isFailResponse( errorResponse ) ) { + const { paymentMethodData } = errorResponse?.meta || {}; + + if ( + objectHasProp( errorResponse, 'message' ) && + isString( errorResponse.message ) && + errorResponse.message.length + ) { + let context: string = noticeContexts.PAYMENTS; + if ( + objectHasProp( errorResponse, 'messageContext' ) && + isString( errorResponse.messageContext ) && + errorResponse.messageContext.length + ) { + context = errorResponse.messageContext; + } createErrorNotice( errorResponse.message, { id: 'wc-payment-error', isDismissible: false, - context: - errorResponse?.messageContext || - noticeContexts.PAYMENTS, + context, } ); } - const { paymentMethodData } = errorResponse?.meta || {}; - if ( billingAddress && isBillingAddress( billingAddress ) ) { + if ( isBillingAddress( billingAddress ) ) { setBillingAddress( billingAddress ); } - dispatch.__internalSetPaymentFailed(); - dispatch.__internalSetPaymentMethodData( paymentMethodData ); - } else if ( errorResponse ) { - if ( errorResponse.message && errorResponse.message.length ) { + + dispatch.__internalSetPaymentMethodData( + isObject( paymentMethodData ) ? paymentMethodData : {} + ); + dispatch.__internalSetPaymentError(); + } else if ( isErrorResponse( errorResponse ) ) { + if ( + objectHasProp( errorResponse, 'message' ) && + isString( errorResponse.message ) && + errorResponse.message.length + ) { + let context: string = noticeContexts.PAYMENTS; + if ( + objectHasProp( errorResponse, 'messageContext' ) && + isString( errorResponse.messageContext ) && + errorResponse.messageContext.length + ) { + context = errorResponse.messageContext; + } createErrorNotice( errorResponse.message, { id: 'wc-payment-error', isDismissible: false, - context: - errorResponse?.messageContext || - noticeContexts.PAYMENTS, + context, } ); } dispatch.__internalSetPaymentError(); - setValidationErrors( errorResponse?.validationErrors ); + + if ( + isValidValidationErrorsObject( + errorResponse.validationErrors + ) + ) { + setValidationErrors( errorResponse.validationErrors ); + } } else { - // otherwise there are no payment methods doing anything so - // just consider success - dispatch.__internalSetPaymentSuccess(); + // Otherwise there are no payment methods doing anything so just assume payment method is ready. + dispatch.__internalSetPaymentReady(); } } ); }; diff --git a/assets/js/data/payment/types.ts b/assets/js/data/payment/types.ts index d4073c7c272..1a831713627 100644 --- a/assets/js/data/payment/types.ts +++ b/assets/js/data/payment/types.ts @@ -5,7 +5,11 @@ import { PlainPaymentMethods, PlainExpressPaymentMethods, } from '@woocommerce/types'; -import type { EmptyObjectType, ObjectType } from '@woocommerce/types'; +import type { + EmptyObjectType, + ObjectType, + FieldValidationStatus, +} from '@woocommerce/types'; import { DataRegistry } from '@wordpress/data'; /** @@ -14,7 +18,6 @@ import { DataRegistry } from '@wordpress/data'; import type { EventObserversType } from '../../base/context/event-emit'; import type { DispatchFromMap } from '../mapped-types'; import * as actions from './actions'; -import { FieldValidationStatus } from '../types'; export interface CustomerPaymentMethodConfiguration { gateway: string; diff --git a/assets/js/data/payment/utils/set-default-payment-method.ts b/assets/js/data/payment/utils/set-default-payment-method.ts index 612284c6748..71fe94e7e76 100644 --- a/assets/js/data/payment/utils/set-default-payment-method.ts +++ b/assets/js/data/payment/utils/set-default-payment-method.ts @@ -60,7 +60,7 @@ export const setDefaultPaymentMethod = async ( return; } - dispatch( PAYMENT_STORE_KEY ).__internalSetPaymentPristine(); + dispatch( PAYMENT_STORE_KEY ).__internalSetPaymentIdle(); dispatch( PAYMENT_STORE_KEY ).__internalSetActivePaymentMethod( paymentMethodKeys[ 0 ] diff --git a/assets/js/data/types.ts b/assets/js/data/types.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/assets/js/data/validation/actions.ts b/assets/js/data/validation/actions.ts index 0dd850a5474..2aef1ab66da 100644 --- a/assets/js/data/validation/actions.ts +++ b/assets/js/data/validation/actions.ts @@ -2,13 +2,13 @@ * External dependencies */ import deprecated from '@wordpress/deprecated'; +import { FieldValidationStatus } from '@woocommerce/types'; /** * Internal dependencies */ import { ACTION_TYPES as types } from './action-types'; import { ReturnOrGeneratorYieldUnion } from '../mapped-types'; -import { FieldValidationStatus } from '../types'; export const setValidationErrors = ( errors: Record< string, FieldValidationStatus > diff --git a/assets/js/data/validation/reducers.ts b/assets/js/data/validation/reducers.ts index a16b8b83274..f3d1f07abac 100644 --- a/assets/js/data/validation/reducers.ts +++ b/assets/js/data/validation/reducers.ts @@ -4,14 +4,13 @@ import type { Reducer } from 'redux'; import { pickBy } from 'lodash'; import isShallowEqual from '@wordpress/is-shallow-equal'; -import { isString } from '@woocommerce/types'; +import { isString, FieldValidationStatus } from '@woocommerce/types'; /** * Internal dependencies */ import { ValidationAction } from './actions'; import { ACTION_TYPES as types } from './action-types'; -import { FieldValidationStatus } from '../types'; const reducer: Reducer< Record< string, FieldValidationStatus > > = ( state: Record< string, FieldValidationStatus > = {}, diff --git a/assets/js/data/validation/test/reducers.ts b/assets/js/data/validation/test/reducers.ts index b99530fcd1c..66ef3b8df71 100644 --- a/assets/js/data/validation/test/reducers.ts +++ b/assets/js/data/validation/test/reducers.ts @@ -1,8 +1,12 @@ +/** + * External dependencies + */ +import { FieldValidationStatus } from '@woocommerce/types'; + /** * Internal dependencies */ import reducer from '../reducers'; -import { FieldValidationStatus } from '../../types'; import { ACTION_TYPES as types } from '.././action-types'; import { ValidationAction } from '../actions'; diff --git a/assets/js/data/validation/test/selectors.ts b/assets/js/data/validation/test/selectors.ts index cb06f3e7660..1703ef76d8c 100644 --- a/assets/js/data/validation/test/selectors.ts +++ b/assets/js/data/validation/test/selectors.ts @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { FieldValidationStatus } from '@woocommerce/types'; + /** * Internal dependencies */ @@ -6,7 +11,6 @@ import { getValidationError, hasValidationErrors, } from '../selectors'; -import { FieldValidationStatus } from '../../types'; describe( 'Validation selectors', () => { it( 'Gets the validation error', () => { diff --git a/assets/js/editor-components/block-title/index.js b/assets/js/editor-components/block-title/index.js index 18ccd0f605d..0827dac567a 100644 --- a/assets/js/editor-components/block-title/index.js +++ b/assets/js/editor-components/block-title/index.js @@ -32,6 +32,7 @@ const BlockTitle = ( { className="wc-block-editor-components-title" value={ heading } onChange={ onChange } + style={ { backgroundColor: 'transparent' } } /> ); diff --git a/assets/js/editor-components/incompatible-payment-gateways-notice/editor.scss b/assets/js/editor-components/incompatible-payment-gateways-notice/editor.scss index db6f6c78b95..5a10598ea7f 100644 --- a/assets/js/editor-components/incompatible-payment-gateways-notice/editor.scss +++ b/assets/js/editor-components/incompatible-payment-gateways-notice/editor.scss @@ -24,6 +24,14 @@ .wc-blocks-incompatible-extensions-notice__element { display: flex; align-items: center; + position: relative; + + &::before { + content: "•"; + position: absolute; + left: -13px; + font-size: 1.2rem; + } } } diff --git a/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx b/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx index b4f8f6f9ec1..1c33b987a35 100644 --- a/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx +++ b/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx @@ -9,7 +9,7 @@ import { createInterpolateElement, useEffect, } from '@wordpress/element'; -import { alert } from '@woocommerce/icons'; +import { Alert } from '@woocommerce/icons'; import { Icon } from '@wordpress/icons'; /** * Internal dependencies @@ -74,7 +74,7 @@ export function IncompatiblePaymentGatewaysNotice( {
} />

{ noticeContent }

diff --git a/assets/js/icons/index.js b/assets/js/icons/index.js index cf8687b350f..70acf6bdd56 100644 --- a/assets/js/icons/index.js +++ b/assets/js/icons/index.js @@ -13,7 +13,7 @@ export { default as woo } from './library/woo'; export { default as miniCart } from './library/mini-cart'; export { default as miniCartAlt } from './library/mini-cart-alt'; export { default as stacks } from './library/stacks'; -export { default as alert } from './library/alert'; +export { default as Alert } from './library/alert'; export { default as customerAccount } from './library/customer-account'; export { default as customerAccountStyle } from './library/customer-account-style'; export { default as customerAccountStyleAlt } from './library/customer-account-style-alt'; diff --git a/assets/js/icons/library/alert.tsx b/assets/js/icons/library/alert.tsx index 096b1329c18..4cec8a9384a 100644 --- a/assets/js/icons/library/alert.tsx +++ b/assets/js/icons/library/alert.tsx @@ -1,18 +1,36 @@ /** * External dependencies */ +import { IconProps } from '@wordpress/icons/build-types/icon'; import { SVG } from '@wordpress/primitives'; -const cart = ( - +interface AlertProps { + status?: 'warning' | 'error' | 'success' | 'info'; + props?: IconProps; +} + +const statusToColorMap = { + warning: '#F0B849', + error: '#CC1818', + success: '#46B450', + info: '#0073AA', +}; + +const Alert = ( { status = 'warning', ...props }: AlertProps ) => ( + - - + + ); -export default cart; +export default Alert; diff --git a/assets/js/types/type-defs/index.ts b/assets/js/types/type-defs/index.ts index 2ee14e71373..3afb7f73155 100644 --- a/assets/js/types/type-defs/index.ts +++ b/assets/js/types/type-defs/index.ts @@ -17,3 +17,4 @@ export * from './utils'; export * from './taxes'; export * from './attributes'; export * from './stock-status'; +export * from './validation'; diff --git a/assets/js/types/type-defs/payment-method-interface.ts b/assets/js/types/type-defs/payment-method-interface.ts index b711c1552b0..4b05bb6e924 100644 --- a/assets/js/types/type-defs/payment-method-interface.ts +++ b/assets/js/types/type-defs/payment-method-interface.ts @@ -166,6 +166,7 @@ export type PaymentMethodInterface = { // Various payment status helpers. paymentStatus: { isPristine: boolean; + isIdle: boolean; isStarted: boolean; isProcessing: boolean; isFinished: boolean; diff --git a/assets/js/types/type-defs/validation.ts b/assets/js/types/type-defs/validation.ts new file mode 100644 index 00000000000..70c9b4000dd --- /dev/null +++ b/assets/js/types/type-defs/validation.ts @@ -0,0 +1,16 @@ +/** + * An interface to describe the validity of a Checkout field. This is what will be stored in the wc/store/validation + * data store. + */ +export interface FieldValidationStatus { + /** + * The message to display to the user. + */ + message: string; + /** + * Whether this validation error should be hidden. Note, hidden errors still prevent checkout. Adding a hidden error + * allows required fields to be validated, but not show the error to the user until they interact with the input + * element, or try to submit the form. + */ + hidden: boolean; +} diff --git a/assets/js/types/type-guards/object.ts b/assets/js/types/type-guards/object.ts index c90646df629..889ab7fc26a 100644 --- a/assets/js/types/type-guards/object.ts +++ b/assets/js/types/type-guards/object.ts @@ -21,3 +21,9 @@ export function objectHasProp< P extends PropertyKey >( // The `in` operator throws a `TypeError` for non-object values. return isObject( target ) && property in target; } + +export const isEmptyObject = < T extends { [ key: string ]: unknown } >( + object: T +) => { + return Object.keys( object ).length === 0; +}; diff --git a/assets/js/types/type-guards/observers.ts b/assets/js/types/type-guards/observers.ts new file mode 100644 index 00000000000..8456473dc87 --- /dev/null +++ b/assets/js/types/type-guards/observers.ts @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { ObserverResponse } from '@woocommerce/base-context'; +import { isObject, objectHasProp } from '@woocommerce/types'; + +/** + * Whether the passed object is an ObserverResponse. + */ +export const isObserverResponse = ( + response: unknown +): response is ObserverResponse => { + return isObject( response ) && objectHasProp( response, 'type' ); +}; diff --git a/assets/js/types/type-guards/test/object.ts b/assets/js/types/type-guards/test/object.ts new file mode 100644 index 00000000000..5ce4e73846c --- /dev/null +++ b/assets/js/types/type-guards/test/object.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { isEmptyObject, isObject } from '@woocommerce/types'; + +describe( 'Object type-guards', () => { + describe( 'Testing isObject()', () => { + it( 'Correctly identifies an object', () => { + expect( isObject( {} ) ).toBe( true ); + expect( isObject( { test: 'object' } ) ).toBe( true ); + } ); + it( 'Correctly rejects object-like things', () => { + expect( isObject( [] ) ).toBe( false ); + expect( isObject( null ) ).toBe( false ); + } ); + } ); + describe( 'Testing isEmptyObject()', () => { + it( 'Correctly identifies an empty object', () => { + expect( isEmptyObject( {} ) ).toBe( true ); + } ); + it( 'Correctly identifies an not empty object', () => { + expect( isEmptyObject( { name: 'Woo' } ) ).toBe( false ); + } ); + } ); +} ); diff --git a/assets/js/types/type-guards/test/validation.ts b/assets/js/types/type-guards/test/validation.ts new file mode 100644 index 00000000000..0a7e2c0f323 --- /dev/null +++ b/assets/js/types/type-guards/test/validation.ts @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import { + isValidFieldValidationStatus, + isValidValidationErrorsObject, +} from '../validation'; + +describe( 'validation type guards', () => { + describe( 'isValidFieldValidationStatus', () => { + it( 'identifies valid objects', () => { + const valid = { + message: 'message', + hidden: false, + }; + expect( isValidFieldValidationStatus( valid ) ).toBe( true ); + } ); + it( 'identifies invalid objects', () => { + const invalid = { + message: 'message', + hidden: 'string', + }; + expect( isValidFieldValidationStatus( invalid ) ).toBe( false ); + const noMessage = { + hidden: false, + }; + expect( isValidFieldValidationStatus( noMessage ) ).toBe( false ); + } ); + } ); + + describe( 'isValidValidationErrorsObject', () => { + it( 'identifies valid objects', () => { + const valid = { + 'billing.first-name': { + message: 'message', + hidden: false, + }, + }; + expect( isValidValidationErrorsObject( valid ) ).toBe( true ); + } ); + it( 'identifies invalid objects', () => { + const invalid = { + 'billing.first-name': { + message: 'message', + hidden: 'string', + }, + }; + expect( isValidValidationErrorsObject( invalid ) ).toBe( false ); + const noMessage = { + 'billing.first-name': { + hidden: false, + }, + }; + expect( isValidValidationErrorsObject( noMessage ) ).toBe( false ); + } ); + } ); +} ); diff --git a/assets/js/types/type-guards/validation.ts b/assets/js/types/type-guards/validation.ts new file mode 100644 index 00000000000..3387a0c7fe4 --- /dev/null +++ b/assets/js/types/type-guards/validation.ts @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { + FieldValidationStatus, + isBoolean, + isObject, + isString, + objectHasProp, +} from '@woocommerce/types'; + +/** + * Whether the given status is a valid FieldValidationStatus. + */ +export const isValidFieldValidationStatus = ( + status: unknown +): status is FieldValidationStatus => { + return ( + isObject( status ) && + objectHasProp( status, 'message' ) && + objectHasProp( status, 'hidden' ) && + isString( status.message ) && + isBoolean( status.hidden ) + ); +}; + +/** + * Whether the passed object is a valid validation errors object. If this is true, it can be set on the + * wc/store/validation store without any issue. + */ +export const isValidValidationErrorsObject = ( + errors: unknown +): errors is Record< string, FieldValidationStatus > => { + return ( + isObject( errors ) && + Object.entries( errors ).every( + ( [ key, value ] ) => + isString( key ) && isValidFieldValidationStatus( value ) + ) + ); +}; diff --git a/docs/README.md b/docs/README.md index c94cf70585e..2d8780bbcb2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -123,8 +123,7 @@ The WooCommerce Blocks Handbook provides documentation for designers and develop ### Tools -- [@woocommerce/extend-cart-checkout-block](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block) This is a template to be used with @wordpress/create-block to create a WooCommerce Blocks extension starting point. -- [How to integrate with inner blocks in the WooCommerce Blocks Checkout](https://github.com/woocommerce/newsletter-test) A repository with some example code showing how an extension can register an inner block for use in the Checkout Block. +- [@woocommerce/extend-cart-checkout-block](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block) This is a template to be used with @wordpress/create-block to create a WooCommerce Blocks extension starting point. It also showcases how to use some extensibility points, e.g. registering an inner block in the Checkout Block, applying filters to certain texts such as the place order button, using Slot/Fill and how to change the behaviour of the Store API. ### Articles diff --git a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md index ad0556e6176..2f85077e151 100644 --- a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md +++ b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md @@ -109,14 +109,13 @@ import { select } from '@wordpress/data'; import { PAYMENT_STORE_KEY } from '@woocommerce/blocks-data'; const MyComponent = ( props ) => { - const isPaymentPristine = select( PAYMENT_STORE_KEY ).isPaymentPristine(); - const isPaymentStarted = select( PAYMENT_STORE_KEY ).isPaymentStarted(); + const isPaymentIdle = select( PAYMENT_STORE_KEY ).isPaymentIdle(); + const isExpressPaymentStarted = + select( PAYMENT_STORE_KEY ).isExpressPaymentStarted(); const isPaymentProcessing = select( PAYMENT_STORE_KEY ).isPaymentProcessing(); - const isPaymentSuccess = select( PAYMENT_STORE_KEY ).isPaymentSuccess(); - const isPaymentFailed = select( PAYMENT_STORE_KEY ).isPaymentFailed(); + const isPaymentReady = select( PAYMENT_STORE_KEY ).isPaymentReady(); const hasPaymentError = select( PAYMENT_STORE_KEY ).hasPaymentError(); - const hasPaymentFinished = select( PAYMENT_STORE_KEY ).hasPaymentFinished(); // do something with the boolean values }; @@ -126,11 +125,10 @@ The status here will help inform the current state of _client side_ processing f The possible _internal_ statuses that may be set are: -- `PRISTINE`: This is the status when checkout is initialized and there are payment methods that are not doing anything. This status is also set whenever the checkout status is changed to `IDLE`. -- `STARTED`: **Express Payment Methods Only** - This status is used when an express payment method has been triggered by the user clicking it's button. This flow happens before processing, usually in a modal window. +- `IDLE`: This is the status when checkout is initialized and there are payment methods that are not doing anything. This status is also set whenever the checkout status is changed to `IDLE`. +- `EXPRESS_STARTED`: **Express Payment Methods Only** - This status is used when an express payment method has been triggered by the user clicking it's button. This flow happens before processing, usually in a modal window. - `PROCESSING`: This status is set when the checkout status is `PROCESSING`, checkout `hasError` is false, checkout is not calculating, and the current payment status is not `FINISHED`. When this status is set, it will trigger the payment processing event emitter. -- `SUCCESS`: This status is set after all the observers hooked into the payment processing event have completed successfully. The `CheckoutProcessor` component uses this along with the checkout `PROCESSING` status to signal things are ready to send the order to the server with data for processing. -- `FAILED`: This status is set after an observer hooked into the payment processing event returns a fail response. This in turn will end up causing the checkout `hasError` flag to be set to true. +- `READY`: This status is set after all the observers hooked into the payment processing event have completed successfully. The `CheckoutProcessor` component uses this along with the checkout `PROCESSING` status to signal things are ready to send the order to the server with data for processing and to take payment - `ERROR`: This status is set after an observer hooked into the payment processing event returns an error response. This in turn will end up causing the checkout `hasError` flag to be set to true. ### Emitting Events diff --git a/docs/internal-developers/block-client-apis/notices.md b/docs/internal-developers/block-client-apis/notices.md index 382f6d84e44..da71886523e 100644 --- a/docs/internal-developers/block-client-apis/notices.md +++ b/docs/internal-developers/block-client-apis/notices.md @@ -5,7 +5,6 @@ - [Notices in WooCommerce Blocks](#notices-in-woocommerce-blocks) - [`StoreNoticesContainer`](#storenoticescontainer) - [Snackbar notices in WooCommerce Blocks](#snackbar-notices-in-woocommerce-blocks) - - [`SnackbarNoticesContainer`](#snackbarnoticescontainer) ## Notices in WooCommerce Blocks @@ -19,6 +18,10 @@ The below example will show all notices with type `default` that are in the `wc/ On the Cart Block, a `StoreNoticesContainer` is already rendered with the `wc/cart` context, and on the Checkout Block, a `StoreNoticesContainer` is already rendered with the `wc/checkout` context. To display errors from other contexts, you can use the `StoreNoticesContainer` component with context passed as a prop. +`StoreNoticesContainer` also support passing an array of context strings to it, this allows you to capture several contexts at once, while filtering out similar notices. + +#### Single context + ```jsx import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; @@ -27,6 +30,23 @@ const PaymentErrors = () => { }; ``` +#### Multiple contexts + +```jsx +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; + +const AddressForm = () => { + return ( + + ); +}; +``` + ## Snackbar notices in WooCommerce Blocks WooCommerce Blocks also shows snackbar notices, to add a snackbar notice you need to create a notice with `type:snackbar` in the options object. @@ -53,17 +73,3 @@ dispatch( 'core/notices' ).createNotice( 'snackbar-notice-id' ); ``` - -### `SnackbarNoticesContainer` - -To display snackbar notices, use the `SnackbarNoticesContainer` component. This component is rendered with the Cart and Checkout blocks, so there is no need to add another. The context it displays notices for is `default`. If, for some reason you do need to show snackbar messages for a different context, you can render this component again and pass the context as a prop to the component. - -```jsx -import { SnackbarNoticesContainer } from '@woocommerce/base-components/snackbar-notices-container'; - -const AlternativeSnackbarNotices = () => { - return ( - - ); -}; -``` diff --git a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index c03549c947d..349e7e65c89 100644 --- a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -86,7 +86,6 @@ We also have individual features or code blocks behind a feature flag, this is a - `__experimentalDeRegisterPaymentMethod` function used to deregister a payment method, only used in tests ([experimental function](https://github.com/woocommerce/woocommerce-blocks/blob/f27456dd00fa0b21b29a935943defb18351edf48/assets/js/blocks-registry/payment-methods/registry.ts#L110-L114)). - `__experimentalDeRegisterExpressPaymentMethod` function used to deregister an express payment method, only used in tests ([experimental function](https://github.com/woocommerce/woocommerce-blocks/blob/f27456dd00fa0b21b29a935943defb18351edf48/assets/js/blocks-registry/payment-methods/registry.ts#L116-L120)). -- `__experimentalRegisterCheckoutFilters` and `__experimentalApplyCheckoutFilter` methods included with `@woocommerce/blocks-checkout` package. They allow registering and applying a filter to certain parts of the Cart and Checkout blocks ([experimental method 1](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/3e59ec9842464f783f6e087947e717fa0b0a7b1b/packages/checkout/registry/index.js#L2) | [experimental method 2](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/3e59ec9842464f783f6e087947e717fa0b0a7b1b/packages/checkout/registry/index.js#L17)). ### Slots @@ -96,7 +95,7 @@ We also have individual features or code blocks behind a feature flag, this is a ### Misc -- `__experimental_woocommerce_blocks_hidden` property in a Cart item data array that allows overwriting the `hidden` property. This is useful to make some cart item data visible/hidden depending if it needs to be displayed in Blocks or shortcode ([experimental property](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/9c4288b0ee46960bdc2bf8ef351d05ac23073b0c/src/StoreApi/Schemas/CartItemSchema.php#L439-L441)). +- `__experimental_woocommerce_blocks_hidden` property allows overwriting the `hidden` property for cart item data. This is useful to make some cart item data visible/hidden depending if it needs to be displayed in the Cart Block or the Cart Shortcode ([experimental property](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/9c4288b0ee46960bdc2bf8ef351d05ac23073b0c/src/StoreApi/Schemas/CartItemSchema.php#L439-L441)). This was added in [this PR](https://github.com/woocommerce/woocommerce-blocks/pull/3732) to resolve [this issue with Subscriptions](https://github.com/woocommerce/woocommerce-blocks/issues/3731). This property will not be needed if the blocks replace the shortcode experience, since in that scenario, the `hidden` property would be sufficient. ## Usages of `experimental` prefix diff --git a/docs/third-party-developers/extensibility/checkout-block/available-filters.md b/docs/third-party-developers/extensibility/checkout-block/available-filters.md index 275ec0ae1ad..0e5093a80c2 100644 --- a/docs/third-party-developers/extensibility/checkout-block/available-filters.md +++ b/docs/third-party-developers/extensibility/checkout-block/available-filters.md @@ -110,11 +110,11 @@ For this example, let's suppose we are building an extension that lets customers const replaceTotalWithDeposit = () => 'Deposit due today'; ``` -2. Now we need to register this filter function, and have it executed when the `totalLabel` filter is applied. We can access the `__experimentalRegisterCheckoutFilters` function on the `window.wc.blocksCheckout` object. As long as your extension's script is enqueued _after_ WooCommerce Blocks' scripts (i.e. by registering `wc-blocks-checkout` as a dependency), then this will be available. +2. Now we need to register this filter function, and have it executed when the `totalLabel` filter is applied. We can access the `registerCheckoutFilters` function on the `window.wc.blocksCheckout` object. As long as your extension's script is enqueued _after_ WooCommerce Blocks' scripts (i.e. by registering `wc-blocks-checkout` as a dependency), then this will be available. ```ts -const { __experimentalRegisterCheckoutFilters } = window.wc.blocksCheckout; -__experimentalRegisterCheckoutFilters( 'my-hypothetical-deposit-plugin', { +const { registerCheckoutFilters } = window.wc.blocksCheckout; +registerCheckoutFilters( 'my-hypothetical-deposit-plugin', { totalLabel: replaceTotalWithDeposit, } ); ``` @@ -140,11 +140,11 @@ const appendTextToPriceInCart = ( value, extensions, args ) => { }; ``` -2. Now we must register it. Refer to the first example for information about `__experimentalRegisterCheckoutFilters`. +2. Now we must register it. Refer to the first example for information about `registerCheckoutFilters`. ```ts -const { __experimentalRegisterCheckoutFilters } = window.wc.blocksCheckout; -__experimentalRegisterCheckoutFilters( 'my-hypothetical-price-plugin', { +const { registerCheckoutFilters } = window.wc.blocksCheckout; +registerCheckoutFilters( 'my-hypothetical-price-plugin', { subtotalPriceFormat: appendTextToPriceInCart, } ); ``` @@ -175,9 +175,9 @@ const filterCoupons = ( coupons ) => { We'd register our filter like this: ```ts -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; +import { registerCheckoutFilters } from '@woocommerce/blocks-checkout'; -__experimentalRegisterCheckoutFilters( 'automatic-coupon-extension', { +registerCheckoutFilters( 'automatic-coupon-extension', { coupons: filterCoupons, } ); ``` @@ -193,9 +193,9 @@ If you want to prevent a coupon apply notice from appearing, you can use the `sh The same can be done with the `showRemoveCouponNotice` filter to prevent a notice when a coupon is removed from the cart. ```ts -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; +import { registerCheckoutFilters } from '@woocommerce/blocks-checkout'; -__experimentalRegisterCheckoutFilters( 'example-extension', { +registerCheckoutFilters( 'example-extension', { showApplyCouponNotice: ( value, extensions, { couponCode } ) => { // Prevent a couponCode called '10off' from creating a notice. return couponCode === '10off' ? false : value; @@ -215,9 +215,9 @@ An important caveat to note is this does _not_ prevent the item from being remov removing it in the Mini Cart, or traditional shortcode cart. ```ts -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; +import { registerCheckoutFilters } from '@woocommerce/blocks-checkout'; -__experimentalRegisterCheckoutFilters( 'example-extension', { +registerCheckoutFilters( 'example-extension', { showRemoveItemLink: ( value, extensions, { cartItem } ) => { // Prevent items with ID 1 being removed from the cart. return cartItem.id !== 1; @@ -235,11 +235,11 @@ Let's assume a merchant want to change the label of the Place Order button _Plac const label = () => `Pay now`; ``` -2. Now we have to register this filter function, and have it executed when the `placeOrderButtonLabel` filter is applied. We can access the `__experimentalRegisterCheckoutFilters` function on the `window.wc.blocksCheckout` object. As long as your extension's script is enqueued _after_ WooCommerce Blocks' scripts (i.e. by registering `wc-blocks-checkout` as a dependency), then this will be available. +2. Now we have to register this filter function, and have it executed when the `placeOrderButtonLabel` filter is applied. We can access the `registerCheckoutFilters` function on the `window.wc.blocksCheckout` object. As long as your extension's script is enqueued _after_ WooCommerce Blocks' scripts (i.e. by registering `wc-blocks-checkout` as a dependency), then this will be available. ```ts -const { __experimentalRegisterCheckoutFilters } = window.wc.blocksCheckout; -__experimentalRegisterCheckoutFilters( 'custom-place-order-button-label', { +const { registerCheckoutFilters } = window.wc.blocksCheckout; +registerCheckoutFilters( 'custom-place-order-button-label', { placeOrderButtonLabel: label, } ); ``` diff --git a/docs/third-party-developers/extensibility/checkout-payment-methods/payment-method-integration.md b/docs/third-party-developers/extensibility/checkout-payment-methods/payment-method-integration.md index 3d086b0bb8b..57b7217b205 100644 --- a/docs/third-party-developers/extensibility/checkout-payment-methods/payment-method-integration.md +++ b/docs/third-party-developers/extensibility/checkout-payment-methods/payment-method-integration.md @@ -180,7 +180,7 @@ A big part of the payment method integration is the interface that is exposed fo | `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | - `isPristine`: This is true when the current payment status is `PRISTINE`. -- `isStarted`: This is true when the current payment status is `STARTED`. +- `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`. - `isProcessing`: This is true when the current payment status is `PROCESSING`. - `isFinished`: This is true when the current payment status is one of `ERROR`, `FAILED`, or`SUCCESS`. - `hasError`: This is true when the current payment status is `ERROR`. diff --git a/docs/third-party-developers/extensibility/data-store/payment.md b/docs/third-party-developers/extensibility/data-store/payment.md index 53d317142c4..be2313a5ccb 100644 --- a/docs/third-party-developers/extensibility/data-store/payment.md +++ b/docs/third-party-developers/extensibility/data-store/payment.md @@ -27,25 +27,42 @@ with. We do not encourage extensions to dispatch actions onto this data store ye ## Selectors -### isPaymentPristine +### (@deprecated) isPaymentPristine -Queries if the status is `pristine` +_**This selector is deprecated and will be removed in a future release. Please use isPaymentIdle instead**_ #### _Returns_ -`boolean`: True if the payment status is `pristine`, false otherwise. +`boolean`: True if the payment status is `idle`, false otherwise. #### _Example_ ```js const store = select( 'wc/store/payment' ); -const isPaymentPristine = store.isPaymentPristine(); +const isPaymentIdle = store.isPaymentIdle(); ``` -### isPaymentStarted +### isPaymentIdle + +Queries if the status is `idle` + +#### _Returns_ + +`boolean`: True if the payment status is `idle`, false otherwise. + +#### _Example_ + +```js +const store = select( 'wc/store/payment' ); +const isPaymentIdle = store.isPaymentIdle(); +``` + +### (@deprecated) isPaymentStarted Queries if the status is `started`. +_**This selector is deprecated and will be removed in a future release. Please use isExpressPaymentStarted instead**_ + #### _Returns_ `boolean`: True if the payment status is `started`, false otherwise. @@ -57,6 +74,23 @@ const store = select( 'wc/store/payment' ); const isPaymentStarted = store.isPaymentStarted(); ``` +### isExpressPaymentStarted + +Queries if an express payment method has been clicked. + +_**This selector is deprecated and will be removed in a future release. Please use isExpressPaymentStarted instead**_ + +#### _Returns_ + +`boolean`: True if the button for an express payment method has been clicked, false otherwise. + +#### _Example_ + +```js +const store = select( 'wc/store/payment' ); +const isPaymentStarted = store.isPaymentStarted(); +``` + ### isPaymentProcessing Queries if the status is `processing`. @@ -72,10 +106,12 @@ const store = select( 'wc/store/payment' ); const isPaymentProcessing = store.isPaymentProcessing(); ``` -### isPaymentSuccess +### (@deprecated) isPaymentSuccess Queries if the status is `success`. +_**This selector is deprecated and will be removed in a future release. Please use isPaymentReady instead**_ + #### _Returns_ `boolean`: True if the payment status is `success`, false otherwise. @@ -87,54 +123,57 @@ const store = select( 'wc/store/payment' ); const isPaymentSuccess = store.isPaymentSuccess(); ``` -### isPaymentFailed +### isPaymentReady -Queries if the status is `failed`. +Queries if the status is `ready`. #### _Returns_ -`boolean`: True if the payment status is `failed`, false otherwise. +`boolean`: True if the payment status is `ready`, false otherwise. #### _Example_ ```js const store = select( 'wc/store/payment' ); -const isPaymentFailed = store.isPaymentFailed(); +const isPaymentReady = store.isPaymentReady(); ``` -### hasPaymentError +### (@deprecated) isPaymentFailed -Queries if the status is `error`. +Queries if the status is `failed`. + +_**This selector is deprecated and will be removed in a future release. Please use hasPaymentError instead**_ #### _Returns_ -`boolean`: True if the payment status is `error`, false otherwise. +`boolean`: True if the payment status is `failed`, false otherwise. #### _Example_ ```js const store = select( 'wc/store/payment' ); -const hasPaymentError = store.hasPaymentError(); +const isPaymentFailed = store.isPaymentFailed(); ``` -### isPaymentFinished +### hasPaymentError -Checks wether the payment has finished processing. This includes failed payments, payments with errors or successful payments. +Queries if the status is `error`. #### _Returns_ -`boolean`: True if the payment status is `success`, `failed` or `error`, false otherwise. +`boolean`: True if the payment status is `error`, false otherwise. #### _Example_ ```js const store = select( 'wc/store/payment' ); -const isPaymentFinished = store.isPaymentFinished(); +const hasPaymentError = store.hasPaymentError(); ``` -### getCurrentStatus (deprecated) +### (@deprecated) getCurrentStatus Returns an object with booleans representing the payment status. + _**This selector is deprecated and will be removed in a future release. Please use the selectors above**_ #### _Returns_ diff --git a/images/block-placeholders/product-image-gallery.svg b/images/block-placeholders/product-image-gallery.svg new file mode 100644 index 00000000000..a9a75f82cf4 --- /dev/null +++ b/images/block-placeholders/product-image-gallery.svg @@ -0,0 +1,9 @@ + + + + Layer 1 + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 96d1f2aea73..06fdcaaf453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "@types/wordpress__blocks": "11.0.7", "@types/wordpress__components": "^23.0.0", "@types/wordpress__core-data": "^2.4.5", - "@types/wordpress__data": "^6.0.1", + "@types/wordpress__data": "^6.0.2", "@types/wordpress__data-controls": "2.2.0", "@types/wordpress__editor": "^11.0.0", "@types/wordpress__notices": "^3.5.0", @@ -11680,9 +11680,10 @@ "license": "MIT" }, "node_modules/@types/wordpress__data": { - "version": "6.0.1", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/wordpress__data/-/wordpress__data-6.0.2.tgz", + "integrity": "sha512-Pu67knXXoTWgCpxTKwePNZz/iKkYe8AQbkkSD/Ba1mw8t4zgEM+jJs5IV5N5ij/awwjs4Subj8mkvS3jMTDwyw==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*", "redux": "^4.1.0" @@ -58730,7 +58731,9 @@ "dev": true }, "@types/wordpress__data": { - "version": "6.0.1", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/wordpress__data/-/wordpress__data-6.0.2.tgz", + "integrity": "sha512-Pu67knXXoTWgCpxTKwePNZz/iKkYe8AQbkkSD/Ba1mw8t4zgEM+jJs5IV5N5ij/awwjs4Subj8mkvS3jMTDwyw==", "dev": true, "requires": { "@types/react": "*", diff --git a/package.json b/package.json index 6b24618f34c..9474f765df1 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "@types/wordpress__blocks": "11.0.7", "@types/wordpress__components": "^23.0.0", "@types/wordpress__core-data": "^2.4.5", - "@types/wordpress__data": "^6.0.1", + "@types/wordpress__data": "^6.0.2", "@types/wordpress__data-controls": "2.2.0", "@types/wordpress__editor": "^11.0.0", "@types/wordpress__notices": "^3.5.0", diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index 08178df360d..d1271174931 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -1,13 +1,14 @@ /** * External dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { PAYMENT_STORE_KEY, STORE_NOTICES_STORE_KEY, } from '@woocommerce/block-data'; import { getNoticeContexts } from '@woocommerce/base-utils'; import type { Notice } from '@wordpress/notices'; +import { useMemo, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -26,9 +27,12 @@ const formatNotices = ( notices: Notice[], context: string ): StoreNotice[] => { const StoreNoticesContainer = ( { className = '', - context, + context = '', additionalNotices = [], }: StoreNoticesContainerProps ): JSX.Element | null => { + const { registerContainer, unregisterContainer } = useDispatch( + STORE_NOTICES_STORE_KEY + ); const { suppressNotices, registeredContainers } = useSelect( ( select ) => ( { suppressNotices: @@ -38,13 +42,17 @@ const StoreNoticesContainer = ( { ).getRegisteredContainers(), } ) ); - + const contexts = useMemo< string[] >( + () => ( Array.isArray( context ) ? context : [ context ] ), + [ context ] + ); // Find sub-contexts that have not been registered. We will show notices from those contexts here too. const allContexts = getNoticeContexts(); const unregisteredSubContexts = allContexts.filter( ( subContext: string ) => - subContext.includes( context + '/' ) && - ! registeredContainers.includes( subContext ) + contexts.some( ( _context: string ) => + subContext.includes( _context + '/' ) + ) && ! registeredContainers.includes( subContext ) ); // Get notices from the current context and any sub-contexts and append the name of the context to the notice @@ -56,13 +64,23 @@ const StoreNoticesContainer = ( { ...unregisteredSubContexts.flatMap( ( subContext: string ) => formatNotices( getNotices( subContext ), subContext ) ), - ...formatNotices( - getNotices( context ).concat( additionalNotices ), - context + ...contexts.flatMap( ( subContext: string ) => + formatNotices( + getNotices( subContext ).concat( additionalNotices ), + subContext + ) ), ].filter( Boolean ) as StoreNotice[]; } ); + // Register the container context with the parent. + useEffect( () => { + contexts.map( ( _context ) => registerContainer( _context ) ); + return () => { + contexts.map( ( _context ) => unregisterContainer( _context ) ); + }; + }, [ contexts, registerContainer, unregisterContainer ] ); + if ( suppressNotices || ! notices.length ) { return null; } @@ -71,7 +89,6 @@ const StoreNoticesContainer = ( { <> notice.type === 'default' ) } diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 8262ff18764..73a6e29ad8a 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -8,7 +8,6 @@ import { sanitizeHTML } from '@woocommerce/utils'; import { useDispatch } from '@wordpress/data'; import { usePrevious } from '@woocommerce/base-hooks'; import { decodeEntities } from '@wordpress/html-entities'; -import { STORE_NOTICES_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -17,19 +16,14 @@ import { getClassNameFromStatus } from './utils'; import type { StoreNotice } from './types'; const StoreNotices = ( { - context, className, notices, }: { - context: string; className: string; notices: StoreNotice[]; } ): JSX.Element => { const ref = useRef< HTMLDivElement >( null ); const { removeNotice } = useDispatch( 'core/notices' ); - const { registerContainer, unregisterContainer } = useDispatch( - STORE_NOTICES_STORE_KEY - ); const noticeIds = notices.map( ( notice ) => notice.id ); const previousNoticeIds = usePrevious( noticeIds ); @@ -65,14 +59,6 @@ const StoreNotices = ( { } }, [ noticeIds, previousNoticeIds, ref ] ); - // Register the container context with the parent. - useEffect( () => { - registerContainer( context ); - return () => { - unregisterContainer( context ); - }; - }, [ context, registerContainer, unregisterContainer ] ); - // Group notices by whether or not they are dismissible. Dismissible notices can be grouped. const dismissibleNotices = notices.filter( ( { isDismissible } ) => !! isDismissible @@ -117,6 +103,17 @@ const StoreNotices = ( { if ( ! noticeGroup.length ) { return null; } + const uniqueNotices = noticeGroup.filter( + ( + notice: Notice, + noticeIndex: number, + noticesArray: Notice[] + ) => + noticesArray.findIndex( + ( _notice: Notice ) => + _notice.content === notice.content + ) === noticeIndex + ); return ( - { noticeGroup.length === 1 ? ( + { uniqueNotices.length === 1 ? ( <> { sanitizeHTML( decodeEntities( @@ -140,7 +137,7 @@ const StoreNotices = ( { ) : (
    - { noticeGroup.map( ( notice ) => ( + { uniqueNotices.map( ( notice ) => (
  • { it( 'Shows notices from unregistered sub-contexts', async () => { dispatch( noticesStore ).createErrorNotice( - 'Custom sub-context error', + 'Custom first sub-context error', { id: 'custom-subcontext-test-error', context: 'wc/checkout/shipping-address', } ); dispatch( noticesStore ).createErrorNotice( - 'Custom sub-context error', + 'Custom second sub-context error', { id: 'custom-subcontext-test-error', context: 'wc/checkout/billing-address', } ); render( ); - // This should match against 3 elements; 2 error messages, and the spoken message where they are combined into one element. + // This should match against 2 messages, one for each sub-context. expect( - screen.getAllByText( /Custom sub-context error/i ) - ).toHaveLength( 3 ); + screen.getAllByText( /Custom first sub-context error/i ) + ).toHaveLength( 2 ); + expect( + screen.getAllByText( /Custom second sub-context error/i ) + ).toHaveLength( 2 ); + // Clean up notices. + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/shipping-address' + ) + ); + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/billing-address' + ) + ); + } ); + + it( 'Shows notices from several contexts', async () => { + dispatch( noticesStore ).createErrorNotice( 'Custom shipping error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/shipping-address', + } ); + dispatch( noticesStore ).createErrorNotice( 'Custom billing error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/billing-address', + } ); + render( + + ); + // This should match against 4 elements; A written and spoken message for each error. + expect( screen.getAllByText( /Custom shipping error/i ) ).toHaveLength( + 2 + ); + expect( screen.getAllByText( /Custom billing error/i ) ).toHaveLength( + 2 + ); + // Clean up notices. + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/shipping-address' + ) + ); + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/billing-address' + ) + ); + } ); + + it( 'Combine same notices from several contexts', async () => { + dispatch( noticesStore ).createErrorNotice( 'Custom generic error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/shipping-address', + } ); + dispatch( noticesStore ).createErrorNotice( 'Custom generic error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/billing-address', + } ); + render( + + ); + // This should match against 2 elements; A written and spoken message. + expect( screen.getAllByText( /Custom generic error/i ) ).toHaveLength( + 2 + ); // Clean up notices. await act( () => dispatch( noticesStore ).removeNotice( diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 332ea079f5d..999c1ac291d 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -8,7 +8,7 @@ import type { export interface StoreNoticesContainerProps { className?: string | undefined; - context: string; + context?: string | string[]; // List of additional notices that were added inline and not stored in the `core/notices` store. additionalNotices?: ( NoticeType & NoticeOptions )[]; } diff --git a/packages/checkout/components/text-input/validated-text-input.tsx b/packages/checkout/components/text-input/validated-text-input.tsx index fb79925bd70..fc8f836a7b1 100644 --- a/packages/checkout/components/text-input/validated-text-input.tsx +++ b/packages/checkout/components/text-input/validated-text-input.tsx @@ -101,6 +101,10 @@ const ValidatedTextInput = ( { inputObject.value = inputObject.value.trim(); inputObject.setCustomValidity( '' ); + if ( previousValue === inputObject.value ) { + return; + } + const inputIsValid = customValidation ? inputObject.checkValidity() && customValidation( inputObject ) : inputObject.checkValidity(); @@ -120,6 +124,7 @@ const ValidatedTextInput = ( { } ); }, [ + previousValue, clearValidationError, customValidation, errorIdString, diff --git a/packages/checkout/filter-registry/README.md b/packages/checkout/filter-registry/README.md index c90ff9f7d1b..31b04780f27 100644 --- a/packages/checkout/filter-registry/README.md +++ b/packages/checkout/filter-registry/README.md @@ -2,12 +2,12 @@ ## Table of Contents -- [\_\_experimentalRegisterCheckoutFilters](#__experimentalregistercheckoutfilters) +- [registerCheckoutFilters](#registercheckoutfilters) - [Usage](#usage) - [Options](#options) - [`namespace (string)`](#namespace-string) - [`filters (object)`](#filters-object) -- [\_\_experimentalApplyCheckoutFilter](#__experimentalapplycheckoutfilter) +- [applyCheckoutFilter](#applycheckoutfilter) - [Usage](#usage-1) - [Options](#options-1) - [`filterName (string, required)`](#filtername-string-required) @@ -19,7 +19,7 @@ The filter registry allows callbacks to be registered to manipulate certain values. This is similar to the traditional filter system in WordPress (where you register a callback with a specific filter and return a modified value). -## \_\_experimentalRegisterCheckoutFilters +## registerCheckoutFilters Registers a callback function with an available filter. This function has the following signature: @@ -44,16 +44,16 @@ type CheckoutFilterFunction = < T >( ```js // Aliased import -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; +import { registerCheckoutFilters } from '@woocommerce/blocks-checkout'; // Global import -// const { __experimentalRegisterCheckoutFilters } = wc.blocksCheckout; + const { registerCheckoutFilters } = wc.blocksCheckout; const callback = ( value ) => { return value; }; -__experimentalRegisterCheckoutFilters( 'my-extension-namespace', { +registerCheckoutFilters( 'my-extension-namespace', { filterName: callback, } ); ``` @@ -74,7 +74,7 @@ A list of filter names and functions (`CheckoutFilterFunction`) to execute when - `extensions` A n object containing extension data. If your extension has extended any of the store's API routes, one of the keys of this object will be your extension's namespace. The value will contain any data you add to the endpoint. Each key in the `extensions` object is an extension namespace, so a third party extension cannot interfere with _your_ extension's schema modifications, unless there is a naming collision, so please ensure your extension has a unique namespace that is unlikely to conflict with other extensions. - `args` - An object containing any additional data passed to the filter function. This usually (but not always) contains at least a key called `context`. The value of `context` will be (at the moment) either `cart` or `checkout`. This is provided to inform extensions about the exact location that the filter is being applied. The same filter can be applied in multiple places. -## \_\_experimentalApplyCheckoutFilter +## applyCheckoutFilter This function applies a filter, and all registered callbacks, to a given value. @@ -82,17 +82,17 @@ This function applies a filter, and all registered callbacks, to a given value. ```js // Aliased import -import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; +import { applyCheckoutFilter } from '@woocommerce/blocks-checkout'; // Global import -// const { __experimentalApplyCheckoutFilter } = wc.blocksCheckout; + const { applyCheckoutFilter } = wc.blocksCheckout; const options = { filterName: 'my-filter', defaultValue: 'Default Value', }; -const filteredValue = __experimentalApplyCheckoutFilter( options ); +const filteredValue = applyCheckoutFilter( options ); ``` ### Options @@ -121,7 +121,7 @@ A function that needs to return true when the filtered value is passed in order ## Available Filters -Filters are implemented throughout the Mini Cart, Cart and Checkout Blocks, as well as some components. For a list of filters, [see this document](../../../docs/third-party-developers/extensibility/checkout-block/available-filters.md). You can also search for [usage of `__experimentalApplyCheckoutFilter` within the source code](https://github.com/woocommerce/woocommerce-gutenberg-products-block/search?q=__experimentalApplyCheckoutFilter). +Filters are implemented throughout the Mini Cart, Cart and Checkout Blocks, as well as some components. For a list of filters, [see this document](../../../docs/third-party-developers/extensibility/checkout-block/available-filters.md). You can also search for [usage of `applyCheckoutFilter` within the source code](https://github.com/woocommerce/woocommerce-gutenberg-products-block/search?q=applyCheckoutFilter). diff --git a/packages/checkout/filter-registry/index.ts b/packages/checkout/filter-registry/index.ts index 91efcbb66a8..7522006f943 100644 --- a/packages/checkout/filter-registry/index.ts +++ b/packages/checkout/filter-registry/index.ts @@ -33,10 +33,11 @@ let checkoutFilters: Record< > = {}; const cachedValues: Record< string, T > = {}; + /** * Register filters for a specific extension. */ -export const __experimentalRegisterCheckoutFilters = ( +export const registerCheckoutFilters = ( namespace: string, filters: Record< string, CheckoutFilterFunction > ): void => { @@ -59,6 +60,24 @@ export const __experimentalRegisterCheckoutFilters = ( }; }; +/** + * Backward compatibility for __experimentalRegisterCheckoutFilters, this has been graduated to stable now. + * Remove after July 2023. + */ +export const __experimentalRegisterCheckoutFilters = ( + namespace: string, + filters: Record< string, CheckoutFilterFunction > +) => { + deprecated( '__experimentalRegisterCheckoutFilters', { + alternative: 'registerCheckoutFilters', + plugin: 'WooCommerce Blocks', + link: '', + since: '6.0.0', + hint: '__experimentalRegisterCheckoutFilters has graduated to stable and this experimental function will be removed.', + } ); + registerCheckoutFilters( namespace, filters ); +}; + /** * Get all filters with a specific name. * @@ -179,7 +198,7 @@ const shouldReRunFilters = < T >( /** * Apply a filter. */ -export const __experimentalApplyCheckoutFilter = < T >( { +export const applyCheckoutFilter = < T >( { filterName, defaultValue, extensions = null, @@ -234,3 +253,41 @@ export const __experimentalApplyCheckoutFilter = < T >( { cachedValues[ filterName ] = value; return value; }; + +/** + * Backward compatibility for __experimentalApplyCheckoutFilter, this has been graduated to stable now. + * Remove after July 2023. + */ +export const __experimentalApplyCheckoutFilter = < T >( { + filterName, + defaultValue, + extensions = null, + arg = null, + validation = returnTrue, +}: { + /** Name of the filter to apply. */ + filterName: string; + /** Default value to filter. */ + defaultValue: T; + /** Values extend to REST API response. */ + extensions?: Record< string, unknown > | null; + /** Object containing arguments for the filter function. */ + arg?: CheckoutFilterArguments; + /** Function that needs to return true when the filtered value is passed in order for the filter to be applied. */ + validation?: ( value: T ) => true | Error; +} ): T => { + deprecated( '__experimentalApplyCheckoutFilter', { + alternative: 'applyCheckoutFilter', + plugin: 'WooCommerce Blocks', + link: '', + since: '6.0.0', + hint: '__experimentalApplyCheckoutFilter has graduated to stable and this experimental function will be removed.', + } ); + return applyCheckoutFilter( { + filterName, + defaultValue, + extensions, + arg, + validation, + } ); +}; diff --git a/packages/checkout/filter-registry/test/admin.js b/packages/checkout/filter-registry/test/admin.js index 50d17783e56..aed5284b817 100644 --- a/packages/checkout/filter-registry/test/admin.js +++ b/packages/checkout/filter-registry/test/admin.js @@ -5,10 +5,7 @@ import { renderHook } from '@testing-library/react-hooks'; /** * Internal dependencies */ -import { - __experimentalRegisterCheckoutFilters, - __experimentalApplyCheckoutFilter, -} from '../'; +import { registerCheckoutFilters, applyCheckoutFilter } from '../'; jest.mock( '@woocommerce/settings', () => { const originalModule = jest.requireActual( '@woocommerce/settings' ); @@ -23,14 +20,14 @@ describe( 'Checkout registry (as admin user)', () => { test( 'should throw if the filter throws and user is an admin', () => { const filterName = 'ErrorTestFilter'; const value = 'Hello World'; - __experimentalRegisterCheckoutFilters( filterName, { + registerCheckoutFilters( filterName, { [ filterName ]: () => { throw new Error( 'test error' ); }, } ); const { result } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName, defaultValue: value, } ) @@ -41,11 +38,11 @@ describe( 'Checkout registry (as admin user)', () => { test( 'should throw if validation throws and user is an admin', () => { const filterName = 'ValidationTestFilter'; const value = 'Hello World'; - __experimentalRegisterCheckoutFilters( filterName, { + registerCheckoutFilters( filterName, { [ filterName ]: ( val ) => val, } ); const { result } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName, defaultValue: value, validation: () => { diff --git a/packages/checkout/filter-registry/test/index.js b/packages/checkout/filter-registry/test/index.js index 0bff625320c..b9a1d1057fb 100644 --- a/packages/checkout/filter-registry/test/index.js +++ b/packages/checkout/filter-registry/test/index.js @@ -5,10 +5,7 @@ import { renderHook } from '@testing-library/react-hooks'; /** * Internal dependencies */ -import { - __experimentalRegisterCheckoutFilters, - __experimentalApplyCheckoutFilter, -} from '../'; +import { registerCheckoutFilters, applyCheckoutFilter } from '../'; describe( 'Checkout registry', () => { const filterName = 'loremIpsum'; @@ -16,7 +13,7 @@ describe( 'Checkout registry', () => { test( 'should return default value if there are no filters', () => { const value = 'Hello World'; const { result: newValue } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName, defaultValue: value, } ) @@ -26,12 +23,12 @@ describe( 'Checkout registry', () => { test( 'should return filtered value when a filter is registered', () => { const value = 'Hello World'; - __experimentalRegisterCheckoutFilters( filterName, { + registerCheckoutFilters( filterName, { [ filterName ]: ( val, extensions, args ) => val.toUpperCase() + args.punctuationSign, } ); const { result: newValue } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName, defaultValue: value, arg: { @@ -45,11 +42,11 @@ describe( 'Checkout registry', () => { test( 'should not return filtered value if validation failed', () => { const value = 'Hello World'; - __experimentalRegisterCheckoutFilters( filterName, { + registerCheckoutFilters( filterName, { [ filterName ]: ( val ) => val.toUpperCase(), } ); const { result: newValue } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName, defaultValue: value, validation: ( val ) => ! val.includes( 'HELLO' ), @@ -69,13 +66,13 @@ describe( 'Checkout registry', () => { // We use this new filter name here to avoid return the cached value for the filter const filterNameThatThrows = 'throw'; const value = 'Hello World'; - __experimentalRegisterCheckoutFilters( filterNameThatThrows, { + registerCheckoutFilters( filterNameThatThrows, { [ filterNameThatThrows ]: () => { throw error; }, } ); const { result: newValue } = renderHook( () => - __experimentalApplyCheckoutFilter( { + applyCheckoutFilter( { filterName: filterNameThatThrows, defaultValue: value, } ) diff --git a/src/BlockTypes/ProductImageGallery.php b/src/BlockTypes/ProductImageGallery.php new file mode 100644 index 00000000000..43e84ebbe32 --- /dev/null +++ b/src/BlockTypes/ProductImageGallery.php @@ -0,0 +1,60 @@ +context['postId']; + global $product; + $product = wc_get_product( $post_id ); + + if ( class_exists( 'WC_Frontend_Scripts' ) ) { + $frontend_scripts = new \WC_Frontend_Scripts(); + $frontend_scripts::load_scripts(); + } + + $classname = $attributes['className'] ?? ''; + + ob_start(); + woocommerce_show_product_images(); + $product_image_gallery_html = ob_get_clean(); + + return sprintf( + '', + esc_attr( $classname ), + $product_image_gallery_html + ); + + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index ad4ea3f4ee6..e9355759ec0 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -187,6 +187,7 @@ protected function get_block_types() { 'ProductCategory', 'ProductCategoryList', 'ProductImage', + 'ProductImageGallery', 'ProductNew', 'ProductOnSale', 'ProductPrice', diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index 658174a2dca..330c9c1c573 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -226,6 +226,7 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { 'postcode' => $customer->get_billing_postcode(), 'country' => $customer->get_billing_country(), 'phone' => $customer->get_billing_phone(), + 'email' => $customer->get_billing_email(), ]; } diff --git a/tests/e2e/specs/backend/__fixtures__/cart.fixture.json b/tests/e2e/specs/backend/__fixtures__/cart.fixture.json index d7414bce4e5..0e4253c9ac1 100644 --- a/tests/e2e/specs/backend/__fixtures__/cart.fixture.json +++ b/tests/e2e/specs/backend/__fixtures__/cart.fixture.json @@ -1 +1 @@ -{"title":"Cart Block","pageContent":"

    You may be interested in…

    \"\"/

    Your cart is currently empty!

    Browse store.


    New in store

    "} +{"title":"Cart Block","pageContent":"

    You may be interested in…

    \"\"/

    Your cart is currently empty!

    Browse store.


    New in store

    "} diff --git a/tests/e2e/specs/shopper/cart-checkout/checkout.test.js b/tests/e2e/specs/shopper/cart-checkout/checkout.test.js index 4aa7a161e85..11e3d50f9bb 100644 --- a/tests/e2e/specs/shopper/cart-checkout/checkout.test.js +++ b/tests/e2e/specs/shopper/cart-checkout/checkout.test.js @@ -304,7 +304,7 @@ describe( 'Shopper → Checkout', () => { await shopper.block.goToCheckout(); await shopper.block.applyCouponFromCheckout( coupon.code ); await page.waitForSelector( - '.wc-block-components-notices__notice' + '.wc-block-components-totals-coupon__content .wc-block-components-validation-error' ); await expect( page ).toMatch( 'Coupon usage limit has been reached.' diff --git a/tests/e2e/specs/shopper/cart-checkout/translations.test.js b/tests/e2e/specs/shopper/cart-checkout/translations.test.js index 3a6c26308ca..c97f4552a8a 100644 --- a/tests/e2e/specs/shopper/cart-checkout/translations.test.js +++ b/tests/e2e/specs/shopper/cart-checkout/translations.test.js @@ -48,7 +48,8 @@ describe( 'Shopper → Cart & Checkout → Translations', () => { ); await expect( orderSummary ).toMatch( 'Subtotaal' ); - await expect( orderSummary ).toMatch( 'Een waardebon code toepassen' ); + // Skipping translation for now, as it's not available in WooCommerce Core. + // await expect( orderSummary ).toMatch( 'Een waardebon toevoegen' ); await expect( orderSummary ).toMatch( 'Totaal' ); } ); @@ -90,7 +91,8 @@ describe( 'Shopper → Cart & Checkout → Translations', () => { ); await expect( orderSummary ).toMatch( 'Besteloverzicht' ); await expect( orderSummary ).toMatch( 'Subtotaal' ); - await expect( orderSummary ).toMatch( 'Een waardebon code toepassen' ); + // Skipping translation for now, as it's not available in WooCommerce Core. + // await expect( orderSummary ).toMatch( 'Een waardebon toevoegen' ); await expect( orderSummary ).toMatch( 'Verzending' ); await expect( orderSummary ).toMatch( 'Totaal' ); } ); diff --git a/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts b/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts index 26740c556d4..9328004f709 100644 --- a/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts +++ b/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts @@ -20,16 +20,17 @@ import { import { addProductQueryBlock, block, - configurateProductQueryBlock, + configureProductQueryBlock, getProductsNameFromClassicTemplate, getProductsNameFromProductQuery, + toggleInheritQueryFromTemplateSetting, } from './utils'; describe( `${ block.name } Block`, () => { useTheme( 'emptytheme' ); describe( 'with All Templates', () => { - beforeAll( async () => { + beforeEach( async () => { const productCatalogTemplateId = 'woocommerce/woocommerce//archive-product'; @@ -41,7 +42,9 @@ describe( `${ block.name } Block`, () => { await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); } ); - it( 'when Inherit Query from template is disabled all the settings that customize the query should be hide', async () => { + it( 'when Inherit Query from template is disabled all the settings that customize the query should be hidden', async () => { + await toggleInheritQueryFromTemplateSetting(); + await expect( page ).toMatchElement( block.selectors.editor.popularFilter ); @@ -51,8 +54,8 @@ describe( `${ block.name } Block`, () => { ); } ); - it( 'when Inherit Query from template is enabled all the settings that customize the query should be hide', async () => { - await configurateProductQueryBlock(); + it( 'when Inherit Query from template is enabled all the settings that customize the query should be hidden', async () => { + await configureProductQueryBlock(); const popularFilterEl = await page.$( block.selectors.editor.popularFilter @@ -75,7 +78,7 @@ describe( `${ block.name } Block`, () => { postId: productCatalogTemplateId, } ); await addProductQueryBlock(); - await configurateProductQueryBlock(); + await configureProductQueryBlock(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -104,7 +107,7 @@ describe( `${ block.name } Block`, () => { postId: taxonomyProductCategory, } ); await addProductQueryBlock(); - await configurateProductQueryBlock(); + await configureProductQueryBlock(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -136,7 +139,7 @@ describe( `${ block.name } Block`, () => { postId: tagProductCategory, } ); await addProductQueryBlock(); - await configurateProductQueryBlock(); + await configureProductQueryBlock(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -168,7 +171,7 @@ describe( `${ block.name } Block`, () => { postId: productSearchResults, } ); await addProductQueryBlock(); - await configurateProductQueryBlock(); + await configureProductQueryBlock(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); diff --git a/tests/e2e/specs/shopper/product-query/utils.ts b/tests/e2e/specs/shopper/product-query/utils.ts index 26b3d232b89..08c86c01b81 100644 --- a/tests/e2e/specs/shopper/product-query/utils.ts +++ b/tests/e2e/specs/shopper/product-query/utils.ts @@ -32,17 +32,16 @@ export const addProductQueryBlock = async () => { await page.waitForNetworkIdle(); }; -const enableInheritQueryFromTemplateSetting = async () => { +export const toggleInheritQueryFromTemplateSetting = async () => { const [ button ] = await page.$x( block.selectors.editor.inheritQueryFromTemplateSetting ); await button.click(); }; -export const configurateProductQueryBlock = async () => { +export const configureProductQueryBlock = async () => { await ensureSidebarOpened(); await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - await enableInheritQueryFromTemplateSetting(); }; export const getProductsNameFromClassicTemplate = async () => { diff --git a/tsconfig.json b/tsconfig.json index 2d88e8033f9..2a1a5b6c03b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "./assets/js/**/*", "./packages/checkout/**/*", "./assets/js/blocks/**/block.json", + "./assets/js/atomic/blocks/**/block.json", "./assets/js/blocks/mini-cart/mini-cart-contents/inner-blocks/**/block.json", "./storybook/**/*", "./tests/js/setup-after-env.ts"