From 312665d698f82c33c15b17f7988f9bcd147fec72 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 20 Jan 2023 18:05:28 +0000 Subject: [PATCH 01/25] Add get_collectible_method_ids function --- src/Shipping/ShippingController.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php index 6e313d99a41..f7be525140e 100644 --- a/src/Shipping/ShippingController.php +++ b/src/Shipping/ShippingController.php @@ -198,6 +198,25 @@ function( $setting ) { return $settings; } + /** + * Gets a list of payment method ids that support the 'collectible' feature. + * + * @return string[] List of payment method ids that support the 'collectible' feature. + */ + public function get_collectible_method_ids() { + return array_keys( + wp_list_pluck( + array_filter( + WC()->shipping()->get_shipping_methods(), + function( $method ) { + return in_array( 'collectible', $method->supports, true ); + } + ), + 'id' + ) + ); + } + /** * Register Local Pickup settings for rest api. */ From a2491e687706e9090ae82adb625cf9ae3c740630 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 20 Jan 2023 18:05:55 +0000 Subject: [PATCH 02/25] Add collectibleMethodIds to asset data registry --- src/Shipping/ShippingController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php index f7be525140e..816c5d5fcb6 100644 --- a/src/Shipping/ShippingController.php +++ b/src/Shipping/ShippingController.php @@ -51,6 +51,7 @@ function() { true ); } + $this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ), true ); $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' ); add_action( 'rest_api_init', [ $this, 'register_settings' ] ); From a9d3d815294982c3a1e845e899c010b961441369 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Tue, 24 Jan 2023 13:07:49 +0000 Subject: [PATCH 03/25] Remove unnecessary pluck and add pickup_location to returned array --- src/Shipping/ShippingController.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php index 816c5d5fcb6..8abb0d0dd1e 100644 --- a/src/Shipping/ShippingController.php +++ b/src/Shipping/ShippingController.php @@ -205,17 +205,19 @@ function( $setting ) { * @return string[] List of payment method ids that support the 'collectible' feature. */ public function get_collectible_method_ids() { - return array_keys( - wp_list_pluck( - array_filter( - WC()->shipping()->get_shipping_methods(), - function( $method ) { - return in_array( 'collectible', $method->supports, true ); - } - ), - 'id' + $collectible_method_ids = array_keys( + array_filter( + WC()->shipping()->get_shipping_methods(), + function( $method ) { + return $method->supports( 'collectible' ); + }, + true ) ); + // `pickup_location` will always be collectible. Adding it here means we can avoid hard coding it elsewhere on + // the client. + $collectible_method_ids[] = 'pickup_location'; + return $collectible_method_ids; } /** From 14092025f340837b5d102cfe67dc1b72b034d1d8 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 23 Jan 2023 17:50:41 +0000 Subject: [PATCH 04/25] Add hasSelectedLocalPickup to shipping types --- assets/js/base/context/hooks/shipping/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/context/hooks/shipping/types.ts b/assets/js/base/context/hooks/shipping/types.ts index 5a132d516df..d191ada754c 100644 --- a/assets/js/base/context/hooks/shipping/types.ts +++ b/assets/js/base/context/hooks/shipping/types.ts @@ -18,6 +18,6 @@ export interface ShippingData { isCollectable: boolean; // True when a rate is currently being selected and persisted to the server. isSelectingRate: boolean; - + // True when the user has chosen a local pickup method. hasSelectedLocalPickup: boolean; } From 1c9c8c6118432e9fba3f27af496901b7153ed551 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Tue, 24 Jan 2023 09:28:52 +0000 Subject: [PATCH 05/25] show shipping address even if collecting --- .../cart-checkout/totals/shipping/index.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index 5b2fade1a9e..946229b0287 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -104,18 +104,17 @@ export const TotalsShipping = ( { - { ! prefersCollection && ( - - ) } + ) : null } From 539d71009627e8258b9f1c1942d578829b369f4b Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 25 Jan 2023 18:23:54 +0000 Subject: [PATCH 06/25] Make checkout store set prefersCollection based on IDs from settings --- assets/js/data/checkout/selectors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/data/checkout/selectors.ts b/assets/js/data/checkout/selectors.ts index 7cf19cb6745..5ccdd7e67b5 100644 --- a/assets/js/data/checkout/selectors.ts +++ b/assets/js/data/checkout/selectors.ts @@ -85,6 +85,7 @@ export const prefersCollection = ( state: CheckoutState ) => { const selectedRate = shippingRates[ 0 ].shipping_rates.find( ( rate ) => rate.selected ); + if ( objectHasProp( selectedRate, 'method_id' ) && isString( selectedRate.method_id ) From 050058ed8b8c2fb14e0e304c6fa2bd678b81608a Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 25 Jan 2023 18:26:23 +0000 Subject: [PATCH 07/25] Move areRatesCollectible outside of hook --- assets/js/base/context/hooks/shipping/use-shipping-data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/base/context/hooks/shipping/use-shipping-data.ts b/assets/js/base/context/hooks/shipping/use-shipping-data.ts index c27c6dc52c5..539226abd54 100644 --- a/assets/js/base/context/hooks/shipping/use-shipping-data.ts +++ b/assets/js/base/context/hooks/shipping/use-shipping-data.ts @@ -85,7 +85,6 @@ export const useShippingData = (): ShippingData => { ( rate ) => rate.split( ':' )[ 0 ] ) ); - // Selects a shipping rate, fires an event, and catch any errors. const { dispatchCheckoutEvent } = useStoreEvents(); const selectShippingRate = useCallback( From b934a0e0ee2b37ebfdfcb770ca0de6b7a76b54d5 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 11:54:11 +0000 Subject: [PATCH 08/25] Add pickup location component --- .../cart-checkout/pickup-location/index.tsx | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 assets/js/base/components/cart-checkout/pickup-location/index.tsx diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx new file mode 100644 index 00000000000..93923a66699 --- /dev/null +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; +import { useSelect } from '@wordpress/data'; +import { isObject, objectHasProp } from '@woocommerce/types'; + +/** + * Shows a formatted pickup location. + */ +const PickupLocation = (): JSX.Element | null => { + const collectibleMethodIds = [ + ...getSetting< string[] >( 'collectibleMethodIds', [] ), + 'pickup_location', + ]; + + const { pickupAddress, pickupMethod } = useSelect( ( select ) => { + const cartShippingRates = select( 'wc/store/cart' ).getShippingRates(); + + const flattenedRates = cartShippingRates.flatMap( + ( cartShippingRate ) => cartShippingRate.shipping_rates + ); + const selectedCollectibleRate = flattenedRates.find( + ( rate ) => + rate.selected && collectibleMethodIds.includes( rate.method_id ) + ); + + // If the rate has an address specified in its metadata. + if ( + isObject( selectedCollectibleRate ) && + objectHasProp( selectedCollectibleRate, 'meta_data' ) + ) { + const selectedRateMetaData = selectedCollectibleRate.meta_data.find( + ( meta ) => meta.key === 'pickup_address' + ); + if ( + isObject( selectedRateMetaData ) && + objectHasProp( selectedRateMetaData, 'value' ) + ) { + const selectedRatePickupAddress = selectedRateMetaData.value; + return { + pickupAddress: selectedRatePickupAddress, + pickupMethod: selectedCollectibleRate.name, + }; + } + } + + if ( isObject( selectedCollectibleRate ) ) { + return { + pickupAddress: undefined, + pickupMethod: selectedCollectibleRate.name, + }; + } + return { + pickupAddress: undefined, + pickupMethod: undefined, + }; + } ); + + // If the method does not contain an address, or the method supporting collection was not found, return early. + if ( + typeof pickupAddress === 'undefined' && + typeof pickupMethod === 'undefined' + ) { + return null; + } + + // Show the pickup method's name if we don't have an address to show. + return ( + + { sprintf( + /* translators: %s: shipping method name, e.g. "Amazon Locker" */ + __( 'Collection from %s', 'woo-gutenberg-products-block' ), + typeof pickupAddress === 'undefined' + ? pickupMethod + : pickupAddress + ) + ' ' } + + ); +}; + +export default PickupLocation; From 807b999065fe1ac3f17d420cf4aeba97ad931f97 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 11:54:30 +0000 Subject: [PATCH 09/25] Show pickup location if user prefers collection --- .../totals/shipping/shipping-address.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx index c2be8f15954..75f0a291ac7 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx @@ -2,12 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { EnteredAddress } from '@woocommerce/settings'; import { formatShippingAddress, isAddressComplete, } from '@woocommerce/base-utils'; import { useEditorContext } from '@woocommerce/base-context'; +import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings'; +import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; /** * Internal dependencies @@ -19,7 +20,8 @@ export interface ShippingAddressProps { showCalculator: boolean; isShippingCalculatorOpen: boolean; setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ]; - shippingAddress: EnteredAddress; + shippingAddress: ShippingAddressType; + prefersCollection?: boolean | undefined; } export const ShippingAddress = ( { @@ -27,6 +29,7 @@ export const ShippingAddress = ( { isShippingCalculatorOpen, setIsShippingCalculatorOpen, shippingAddress, + prefersCollection = false, }: ShippingAddressProps ): JSX.Element | null => { const addressComplete = isAddressComplete( shippingAddress ); const { isEditor } = useEditorContext(); @@ -38,7 +41,11 @@ export const ShippingAddress = ( { const formattedLocation = formatShippingAddress( shippingAddress ); return ( <> - + { prefersCollection ? ( + + ) : ( + + ) } { showCalculator && ( Date: Thu, 26 Jan 2023 12:02:45 +0000 Subject: [PATCH 10/25] Move prefersCollection check into ShippingAddress component --- .../components/cart-checkout/totals/shipping/index.tsx | 9 --------- .../cart-checkout/totals/shipping/shipping-address.tsx | 8 +++++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index 946229b0287..6fdb4b97cb7 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -8,8 +8,6 @@ import { useStoreCart } from '@woocommerce/base-context/hooks'; import { TotalsItem } from '@woocommerce/blocks-checkout'; import type { Currency } from '@woocommerce/price-format'; import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via'; -import { useSelect } from '@wordpress/data'; -import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { isAddressComplete } from '@woocommerce/base-utils'; /** @@ -50,12 +48,6 @@ export const TotalsShipping = ( { shippingRates, isLoadingRates, } = useStoreCart(); - const { prefersCollection } = useSelect( ( select ) => { - const checkoutStore = select( CHECKOUT_STORE_KEY ); - return { - prefersCollection: checkoutStore.prefersCollection(), - }; - } ); const totalShippingValue = getTotalShippingValue( values ); const hasRates = hasShippingRate( shippingRates ) || totalShippingValue > 0; const showShippingCalculatorForm = @@ -113,7 +105,6 @@ export const TotalsShipping = ( { setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen } - prefersCollection={ prefersCollection } /> ) : null diff --git a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx index 75f0a291ac7..79b58efeaf3 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx @@ -9,6 +9,8 @@ import { import { useEditorContext } from '@woocommerce/base-context'; import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings'; import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -21,7 +23,6 @@ export interface ShippingAddressProps { isShippingCalculatorOpen: boolean; setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ]; shippingAddress: ShippingAddressType; - prefersCollection?: boolean | undefined; } export const ShippingAddress = ( { @@ -29,11 +30,12 @@ export const ShippingAddress = ( { isShippingCalculatorOpen, setIsShippingCalculatorOpen, shippingAddress, - prefersCollection = false, }: ShippingAddressProps ): JSX.Element | null => { const addressComplete = isAddressComplete( shippingAddress ); const { isEditor } = useEditorContext(); - + const prefersCollection = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).prefersCollection() + ); // If the address is incomplete, and we're not in the editor, don't show anything. if ( ! addressComplete && ! isEditor ) { return null; From 0187905b87fa7da477f59f4466cc0e50209fb6cd Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 14:11:36 +0000 Subject: [PATCH 11/25] Remove spread for collectibleMethodIds Not needed now since pickup_location is included in the setting by default --- .../components/cart-checkout/pickup-location/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx index 93923a66699..6b90f1d0256 100644 --- a/assets/js/base/components/cart-checkout/pickup-location/index.tsx +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -10,10 +10,10 @@ import { isObject, objectHasProp } from '@woocommerce/types'; * Shows a formatted pickup location. */ const PickupLocation = (): JSX.Element | null => { - const collectibleMethodIds = [ - ...getSetting< string[] >( 'collectibleMethodIds', [] ), - 'pickup_location', - ]; + const collectibleMethodIds = getSetting< string[] >( + 'collectibleMethodIds', + [] + ); const { pickupAddress, pickupMethod } = useSelect( ( select ) => { const cartShippingRates = select( 'wc/store/cart' ).getShippingRates(); From c03a22cb8cb94f9f4eba8800f0dcc9121ee0e603 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 14:50:23 +0000 Subject: [PATCH 12/25] Check address metadata has a value before displaying it --- .../js/base/components/cart-checkout/pickup-location/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx index 6b90f1d0256..46ffc7bfb4a 100644 --- a/assets/js/base/components/cart-checkout/pickup-location/index.tsx +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -36,7 +36,8 @@ const PickupLocation = (): JSX.Element | null => { ); if ( isObject( selectedRateMetaData ) && - objectHasProp( selectedRateMetaData, 'value' ) + objectHasProp( selectedRateMetaData, 'value' ) && + selectedRateMetaData.value ) { const selectedRatePickupAddress = selectedRateMetaData.value; return { From 7f92bd731c1f708cfc9ffa0947b40c1f0b49a6c7 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 14:52:40 +0000 Subject: [PATCH 13/25] Add tests for ShippingAddress component --- .../totals/shipping/test/shipping-address.tsx | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx new file mode 100644 index 00000000000..f6e204b954e --- /dev/null +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx @@ -0,0 +1,134 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import ShippingAddress from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-address'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { dispatch } from '@wordpress/data'; +import { previewCart } from '@woocommerce/resource-previews'; + +jest.mock( '@woocommerce/settings', () => { + const originalModule = jest.requireActual( '@woocommerce/settings' ); + + return { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore We know @woocommerce/settings is an object. + ...originalModule, + getSetting: ( setting: string, ...rest: unknown[] ) => { + if ( setting === 'localPickupEnabled' ) { + return true; + } + if ( setting === 'collectibleMethodIds' ) { + return [ 'pickup_location' ]; + } + return originalModule.getSetting( setting, ...rest ); + }, + }; +} ); +describe( 'ShippingAddress', () => { + const testShippingAddress = { + first_name: 'John', + last_name: 'Doe', + company: 'Automattic', + address_1: '123 Main St', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94107', + country: 'US', + phone: '555-555-5555', + }; + + it( 'renders ShippingLocation if user does not prefer collection', () => { + render( + + ); + expect( screen.getByText( /Shipping to 94107/ ) ).toBeInTheDocument(); + expect( + screen.queryByText( /Collection from/ ) + ).not.toBeInTheDocument(); + } ); + it( 'renders PickupLocation if shopper prefers collection and shows address if set', async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.method_id === 'pickup_location' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( + + ); + expect( + screen.getByText( + /Collection from 123 Easy Street, New York, 12345/ + ) + ).toBeInTheDocument(); + } ); + it( 'renders PickupLocation if shopper prefers collection and only shows name if address is not set', async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.rate_id === 'pickup_location:2' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + // Set the pickup_location metadata value to an empty string in the selected pickup rate. + const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data.findIndex( + ( metaData ) => metaData.key === 'pickup_address' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data[ addressKeyIndex ].value = ''; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( + + ); + expect( + screen.getByText( /Collection from Local pickup/ ) + ).toBeInTheDocument(); + } ); +} ); From a40523566b81dd4d4f9fe72e8065b287e6856825 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 15:02:12 +0000 Subject: [PATCH 14/25] Move PickupLocation specific tests to new file --- .../pickup-location/test/index.tsx | 93 +++++++++++++++++++ .../totals/shipping/test/shipping-address.tsx | 45 +-------- 2 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 assets/js/base/components/cart-checkout/pickup-location/test/index.tsx diff --git a/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx new file mode 100644 index 00000000000..0ae5213bea6 --- /dev/null +++ b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { dispatch } from '@wordpress/data'; +import { previewCart } from '@woocommerce/resource-previews'; +import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; + +jest.mock( '@woocommerce/settings', () => { + const originalModule = jest.requireActual( '@woocommerce/settings' ); + + return { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore We know @woocommerce/settings is an object. + ...originalModule, + getSetting: ( setting: string, ...rest: unknown[] ) => { + if ( setting === 'localPickupEnabled' ) { + return true; + } + if ( setting === 'collectibleMethodIds' ) { + return [ 'pickup_location' ]; + } + return originalModule.getSetting( setting, ...rest ); + }, + }; +} ); +describe( 'PickupLocation', () => { + it( `renders an address if one is set in the method's metadata`, async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.method_id === 'pickup_location' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( ); + expect( + screen.getByText( + /Collection from 123 Easy Street, New York, 12345/ + ) + ).toBeInTheDocument(); + } ); + it( 'renders the method name if address is not in metadata', async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.rate_id === 'pickup_location:2' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + // Set the pickup_location metadata value to an empty string in the selected pickup rate. + const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data.findIndex( + ( metaData ) => metaData.key === 'pickup_address' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data[ addressKeyIndex ].value = ''; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( ); + expect( + screen.getByText( /Collection from Local pickup/ ) + ).toBeInTheDocument(); + } ); +} ); diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx index f6e204b954e..1678396e883 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx @@ -53,7 +53,7 @@ describe( 'ShippingAddress', () => { screen.queryByText( /Collection from/ ) ).not.toBeInTheDocument(); } ); - it( 'renders PickupLocation if shopper prefers collection and shows address if set', async () => { + it( 'renders PickupLocation if shopper prefers collection', async () => { dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); // Deselect the default selected rate and select pickup_location:1 rate. @@ -88,47 +88,4 @@ describe( 'ShippingAddress', () => { ) ).toBeInTheDocument(); } ); - it( 'renders PickupLocation if shopper prefers collection and only shows name if address is not set', async () => { - dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); - - // Deselect the default selected rate and select pickup_location:1 rate. - const currentlySelectedIndex = - previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( - ( rate ) => rate.selected - ); - previewCart.shipping_rates[ 0 ].shipping_rates[ - currentlySelectedIndex - ].selected = false; - const pickupRateIndex = - previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( - ( rate ) => rate.rate_id === 'pickup_location:2' - ); - previewCart.shipping_rates[ 0 ].shipping_rates[ - pickupRateIndex - ].selected = true; - - // Set the pickup_location metadata value to an empty string in the selected pickup rate. - const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[ - pickupRateIndex - ].meta_data.findIndex( - ( metaData ) => metaData.key === 'pickup_address' - ); - previewCart.shipping_rates[ 0 ].shipping_rates[ - pickupRateIndex - ].meta_data[ addressKeyIndex ].value = ''; - - dispatch( CART_STORE_KEY ).receiveCart( previewCart ); - - render( - - ); - expect( - screen.getByText( /Collection from Local pickup/ ) - ).toBeInTheDocument(); - } ); } ); From 0bdabaedf3f6c32f8f39fed016de7ec8cd25c055 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 26 Jan 2023 16:11:02 +0000 Subject: [PATCH 15/25] Ensure TotalsShipping shows only one package rate if local pickup chosen --- .../cart-checkout/totals/shipping/index.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index 6fdb4b97cb7..b419b36c136 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -9,6 +9,9 @@ import { TotalsItem } from '@woocommerce/blocks-checkout'; import type { Currency } from '@woocommerce/price-format'; import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via'; import { isAddressComplete } from '@woocommerce/base-utils'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; +import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies @@ -32,6 +35,10 @@ export interface TotalShippingProps { isCheckout?: boolean; } +const collectibleMethodIds = getSetting< string[] >( + 'collectibleMethodIds', + [] +); export const TotalsShipping = ( { currency, values, @@ -52,10 +59,21 @@ export const TotalsShipping = ( { const hasRates = hasShippingRate( shippingRates ) || totalShippingValue > 0; const showShippingCalculatorForm = showCalculator && isShippingCalculatorOpen; + const prefersCollection = useSelect( ( select ) => { + return select( CHECKOUT_STORE_KEY ).prefersCollection(); + } ); const selectedShippingRates = shippingRates.flatMap( ( shippingPackage ) => { return shippingPackage.shipping_rates - .filter( ( rate ) => rate.selected ) + .filter( + ( rate ) => + // If the shopper prefers collection, the rate is collectible AND selected. + ( prefersCollection && + collectibleMethodIds.includes( rate.method_id ) && + rate.selected ) || + // Or the shopper does not prefer collection and the rate is selected + ( ! prefersCollection && rate.selected ) + ) .flatMap( ( rate ) => rate.name ); } ); From d910956858b8ddc2f0a9ea6e5632935f19fb1618 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 09:49:17 -0800 Subject: [PATCH 16/25] Update prefersCollection selector to use typeof check --- assets/js/data/checkout/selectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/data/checkout/selectors.ts b/assets/js/data/checkout/selectors.ts index 5ccdd7e67b5..9982215995e 100644 --- a/assets/js/data/checkout/selectors.ts +++ b/assets/js/data/checkout/selectors.ts @@ -77,7 +77,7 @@ export const isCalculating = ( state: CheckoutState ) => { }; export const prefersCollection = ( state: CheckoutState ) => { - if ( state.prefersCollection === undefined ) { + if ( typeof state.prefersCollection === 'undefined' ) { const shippingRates = select( cartStoreKey ).getShippingRates(); if ( ! shippingRates || ! shippingRates.length ) { return false; From 03ac4d07994c901cd53d15d559dfc82efc106845 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:01:38 -0800 Subject: [PATCH 17/25] Use isPackageRateCollectible rather than checking against settings --- .../components/cart-checkout/pickup-location/index.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx index 46ffc7bfb4a..131f2b8c69a 100644 --- a/assets/js/base/components/cart-checkout/pickup-location/index.tsx +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -2,19 +2,14 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { getSetting } from '@woocommerce/settings'; import { useSelect } from '@wordpress/data'; import { isObject, objectHasProp } from '@woocommerce/types'; +import { isPackageRateCollectable } from '@woocommerce/base-utils'; /** * Shows a formatted pickup location. */ const PickupLocation = (): JSX.Element | null => { - const collectibleMethodIds = getSetting< string[] >( - 'collectibleMethodIds', - [] - ); - const { pickupAddress, pickupMethod } = useSelect( ( select ) => { const cartShippingRates = select( 'wc/store/cart' ).getShippingRates(); @@ -22,8 +17,7 @@ const PickupLocation = (): JSX.Element | null => { ( cartShippingRate ) => cartShippingRate.shipping_rates ); const selectedCollectibleRate = flattenedRates.find( - ( rate ) => - rate.selected && collectibleMethodIds.includes( rate.method_id ) + ( rate ) => rate.selected && isPackageRateCollectable( rate ) ); // If the rate has an address specified in its metadata. From 76c2ce50f6fd9221231c6282018195ee75bc47d7 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:01:52 -0800 Subject: [PATCH 18/25] Do not show calculator button if local pickup rate is selected --- .../cart-checkout/totals/shipping/shipping-address.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx index 79b58efeaf3..2d95810aa88 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx @@ -48,7 +48,7 @@ export const ShippingAddress = ( { ) : ( ) } - { showCalculator && ( + { showCalculator && ! prefersCollection ? ( - ) } + ) : null } ); }; From 9522c5738b4933ac0699b32dc68cfb32de59b888 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:06:59 -0800 Subject: [PATCH 19/25] Update test to mock correct setting --- .../components/cart-checkout/pickup-location/test/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx index 0ae5213bea6..603bb951931 100644 --- a/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx +++ b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx @@ -18,7 +18,7 @@ jest.mock( '@woocommerce/settings', () => { if ( setting === 'localPickupEnabled' ) { return true; } - if ( setting === 'collectibleMethodIds' ) { + if ( setting === 'collectableMethodIds' ) { return [ 'pickup_location' ]; } return originalModule.getSetting( setting, ...rest ); From cc705ab62518e41827da16b5d1d1f80864362d77 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:07:10 -0800 Subject: [PATCH 20/25] Remove unused method from ShippingController --- src/Shipping/ShippingController.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php index 8abb0d0dd1e..e6be896593f 100644 --- a/src/Shipping/ShippingController.php +++ b/src/Shipping/ShippingController.php @@ -199,27 +199,6 @@ function( $setting ) { return $settings; } - /** - * Gets a list of payment method ids that support the 'collectible' feature. - * - * @return string[] List of payment method ids that support the 'collectible' feature. - */ - public function get_collectible_method_ids() { - $collectible_method_ids = array_keys( - array_filter( - WC()->shipping()->get_shipping_methods(), - function( $method ) { - return $method->supports( 'collectible' ); - }, - true - ) - ); - // `pickup_location` will always be collectible. Adding it here means we can avoid hard coding it elsewhere on - // the client. - $collectible_method_ids[] = 'pickup_location'; - return $collectible_method_ids; - } - /** * Register Local Pickup settings for rest api. */ From 9869fc7061043d56b35a3f4b875d5bcac2d126da Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:08:02 -0800 Subject: [PATCH 21/25] Check isPackageRateCollectable rather than checking settings array --- .../cart-checkout/totals/shipping/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index b419b36c136..08818aea030 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -11,7 +11,7 @@ import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/s import { isAddressComplete } from '@woocommerce/base-utils'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { useSelect } from '@wordpress/data'; -import { getSetting } from '@woocommerce/settings'; +import { isPackageRateCollectable } from '@woocommerce/base-utils'; /** * Internal dependencies @@ -34,11 +34,6 @@ export interface TotalShippingProps { className?: string; isCheckout?: boolean; } - -const collectibleMethodIds = getSetting< string[] >( - 'collectibleMethodIds', - [] -); export const TotalsShipping = ( { currency, values, @@ -67,9 +62,9 @@ export const TotalsShipping = ( { return shippingPackage.shipping_rates .filter( ( rate ) => - // If the shopper prefers collection, the rate is collectible AND selected. + // If the shopper prefers collection, the rate is collectable AND selected. ( prefersCollection && - collectibleMethodIds.includes( rate.method_id ) && + isPackageRateCollectable( rate ) && rate.selected ) || // Or the shopper does not prefer collection and the rate is selected ( ! prefersCollection && rate.selected ) From b16668533c505c6da4bb9d6d34956b873d581291 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:08:24 -0800 Subject: [PATCH 22/25] Update test to mock correct setting --- .../cart-checkout/totals/shipping/test/shipping-address.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx index 1678396e883..c3be82cf4cd 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx @@ -18,7 +18,7 @@ jest.mock( '@woocommerce/settings', () => { if ( setting === 'localPickupEnabled' ) { return true; } - if ( setting === 'collectibleMethodIds' ) { + if ( setting === 'collectableMethodIds' ) { return [ 'pickup_location' ]; } return originalModule.getSetting( setting, ...rest ); From edab089276856756f8127386c5536dfcc99ad258 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 10 Mar 2023 10:09:11 -0800 Subject: [PATCH 23/25] Change spelling of collectible to collectable --- .../cart-checkout/pickup-location/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx index 131f2b8c69a..a213f795f71 100644 --- a/assets/js/base/components/cart-checkout/pickup-location/index.tsx +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -16,16 +16,16 @@ const PickupLocation = (): JSX.Element | null => { const flattenedRates = cartShippingRates.flatMap( ( cartShippingRate ) => cartShippingRate.shipping_rates ); - const selectedCollectibleRate = flattenedRates.find( + const selectedCollectableRate = flattenedRates.find( ( rate ) => rate.selected && isPackageRateCollectable( rate ) ); // If the rate has an address specified in its metadata. if ( - isObject( selectedCollectibleRate ) && - objectHasProp( selectedCollectibleRate, 'meta_data' ) + isObject( selectedCollectableRate ) && + objectHasProp( selectedCollectableRate, 'meta_data' ) ) { - const selectedRateMetaData = selectedCollectibleRate.meta_data.find( + const selectedRateMetaData = selectedCollectableRate.meta_data.find( ( meta ) => meta.key === 'pickup_address' ); if ( @@ -36,15 +36,15 @@ const PickupLocation = (): JSX.Element | null => { const selectedRatePickupAddress = selectedRateMetaData.value; return { pickupAddress: selectedRatePickupAddress, - pickupMethod: selectedCollectibleRate.name, + pickupMethod: selectedCollectableRate.name, }; } } - if ( isObject( selectedCollectibleRate ) ) { + if ( isObject( selectedCollectableRate ) ) { return { pickupAddress: undefined, - pickupMethod: selectedCollectibleRate.name, + pickupMethod: selectedCollectableRate.name, }; } return { From 1275757700e806149a9779590e0c36bdf9efdf5a Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 27 Mar 2023 16:56:08 +0100 Subject: [PATCH 24/25] Improve mocked useSelect function Old one returned incorrect data shape for prefersCollection --- .../totals/shipping/test/index.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx index daa43f4a967..ed04a2eb123 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx @@ -18,9 +18,24 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); -wpData.useSelect.mockImplementation( () => { - return { prefersCollection: false }; -} ); +// Mock use select so we can override it when wc/store/checkout is accessed, but return the original select function if any other store is accessed. +wpData.useSelect.mockImplementation( + jest.fn().mockImplementation( ( passedMapSelect ) => { + const mockedSelect = jest.fn().mockImplementation( ( storeName ) => { + if ( storeName === 'wc/store/checkout' ) { + return { + prefersCollection() { + return false; + }, + }; + } + return jest.requireActual( '@wordpress/data' ).select( storeName ); + } ); + passedMapSelect( mockedSelect, { + dispatch: jest.requireActual( '@wordpress/data' ).dispatch, + } ); + } ) +); const shippingAddress = { first_name: 'John', From b5191028e42ccf88992c368aa7ce0733ef7655f1 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 27 Mar 2023 17:05:06 +0100 Subject: [PATCH 25/25] Remove duplicate import --- .../base/components/cart-checkout/totals/shipping/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index 08818aea030..6085c4d15d6 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -8,10 +8,12 @@ import { useStoreCart } from '@woocommerce/base-context/hooks'; import { TotalsItem } from '@woocommerce/blocks-checkout'; import type { Currency } from '@woocommerce/price-format'; import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via'; -import { isAddressComplete } from '@woocommerce/base-utils'; +import { + isAddressComplete, + isPackageRateCollectable, +} from '@woocommerce/base-utils'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { useSelect } from '@wordpress/data'; -import { isPackageRateCollectable } from '@woocommerce/base-utils'; /** * Internal dependencies