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..a213f795f71
--- /dev/null
+++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx
@@ -0,0 +1,78 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+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 { pickupAddress, pickupMethod } = useSelect( ( select ) => {
+ const cartShippingRates = select( 'wc/store/cart' ).getShippingRates();
+
+ const flattenedRates = cartShippingRates.flatMap(
+ ( cartShippingRate ) => cartShippingRate.shipping_rates
+ );
+ const selectedCollectableRate = flattenedRates.find(
+ ( rate ) => rate.selected && isPackageRateCollectable( rate )
+ );
+
+ // If the rate has an address specified in its metadata.
+ if (
+ isObject( selectedCollectableRate ) &&
+ objectHasProp( selectedCollectableRate, 'meta_data' )
+ ) {
+ const selectedRateMetaData = selectedCollectableRate.meta_data.find(
+ ( meta ) => meta.key === 'pickup_address'
+ );
+ if (
+ isObject( selectedRateMetaData ) &&
+ objectHasProp( selectedRateMetaData, 'value' ) &&
+ selectedRateMetaData.value
+ ) {
+ const selectedRatePickupAddress = selectedRateMetaData.value;
+ return {
+ pickupAddress: selectedRatePickupAddress,
+ pickupMethod: selectedCollectableRate.name,
+ };
+ }
+ }
+
+ if ( isObject( selectedCollectableRate ) ) {
+ return {
+ pickupAddress: undefined,
+ pickupMethod: selectedCollectableRate.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;
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..603bb951931
--- /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 === 'collectableMethodIds' ) {
+ 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/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/index.tsx
index 5b2fade1a9e..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,9 +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 { useSelect } from '@wordpress/data';
+import {
+ isAddressComplete,
+ isPackageRateCollectable,
+} from '@woocommerce/base-utils';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
-import { isAddressComplete } from '@woocommerce/base-utils';
+import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -33,7 +36,6 @@ export interface TotalShippingProps {
className?: string;
isCheckout?: boolean;
}
-
export const TotalsShipping = ( {
currency,
values,
@@ -50,20 +52,25 @@ 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 =
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 collectable AND selected.
+ ( prefersCollection &&
+ isPackageRateCollectable( rate ) &&
+ rate.selected ) ||
+ // Or the shopper does not prefer collection and the rate is selected
+ ( ! prefersCollection && rate.selected )
+ )
.flatMap( ( rate ) => rate.name );
}
);
@@ -104,18 +111,16 @@ export const TotalsShipping = ( {
- { ! 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 c2be8f15954..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
@@ -2,12 +2,15 @@
* 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';
+import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -19,7 +22,7 @@ export interface ShippingAddressProps {
showCalculator: boolean;
isShippingCalculatorOpen: boolean;
setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ];
- shippingAddress: EnteredAddress;
+ shippingAddress: ShippingAddressType;
}
export const ShippingAddress = ( {
@@ -30,7 +33,9 @@ export const ShippingAddress = ( {
}: 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;
@@ -38,8 +43,12 @@ export const ShippingAddress = ( {
const formattedLocation = formatShippingAddress( shippingAddress );
return (
<>
-
- { showCalculator && (
+ { prefersCollection ? (
+
+ ) : (
+
+ ) }
+ { showCalculator && ! prefersCollection ? (
- ) }
+ ) : null }
>
);
};
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',
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..c3be82cf4cd
--- /dev/null
+++ b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx
@@ -0,0 +1,91 @@
+/**
+ * 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 === 'collectableMethodIds' ) {
+ 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', 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();
+ } );
+} );
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;
}
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(
diff --git a/assets/js/data/checkout/selectors.ts b/assets/js/data/checkout/selectors.ts
index 7cf19cb6745..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;
@@ -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 )
diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php
index 6e313d99a41..e6be896593f 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' ] );