Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add "Collection from..." in Checkout sidebar when selecting local pickup #8305

Merged
merged 25 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
312665d
Add get_collectible_method_ids function
opr Jan 20, 2023
a2491e6
Add collectibleMethodIds to asset data registry
opr Jan 20, 2023
a9d3d81
Remove unnecessary pluck and add pickup_location to returned array
opr Jan 24, 2023
1409202
Add hasSelectedLocalPickup to shipping types
opr Jan 23, 2023
1c9c8c6
show shipping address even if collecting
opr Jan 24, 2023
539d710
Make checkout store set prefersCollection based on IDs from settings
opr Jan 25, 2023
050058e
Move areRatesCollectible outside of hook
opr Jan 25, 2023
b934a0e
Add pickup location component
opr Jan 26, 2023
807b999
Show pickup location if user prefers collection
opr Jan 26, 2023
02b4a2c
Move prefersCollection check into ShippingAddress component
opr Jan 26, 2023
0187905
Remove spread for collectibleMethodIds
opr Jan 26, 2023
c03a22c
Check address metadata has a value before displaying it
opr Jan 26, 2023
7f92bd7
Add tests for ShippingAddress component
opr Jan 26, 2023
a405235
Move PickupLocation specific tests to new file
opr Jan 26, 2023
0bdabae
Ensure TotalsShipping shows only one package rate if local pickup chosen
opr Jan 26, 2023
d910956
Update prefersCollection selector to use typeof check
opr Mar 10, 2023
03ac4d0
Use isPackageRateCollectible rather than checking against settings
opr Mar 10, 2023
76c2ce5
Do not show calculator button if local pickup rate is selected
opr Mar 10, 2023
9522c57
Update test to mock correct setting
opr Mar 10, 2023
cc705ab
Remove unused method from ShippingController
opr Mar 10, 2023
9869fc7
Check isPackageRateCollectable rather than checking settings array
opr Mar 10, 2023
b166685
Update test to mock correct setting
opr Mar 10, 2023
edab089
Change spelling of collectible to collectable
opr Mar 10, 2023
1275757
Improve mocked useSelect function
opr Mar 27, 2023
b519102
Remove duplicate import
opr Mar 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions assets/js/base/components/cart-checkout/pickup-location/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<span className="wc-block-components-shipping-address">
{ sprintf(
/* translators: %s: shipping method name, e.g. "Amazon Locker" */
__( 'Collection from %s', 'woo-gutenberg-products-block' ),
typeof pickupAddress === 'undefined'
? pickupMethod
: pickupAddress
) + ' ' }
</span>
);
};

export default PickupLocation;
Original file line number Diff line number Diff line change
@@ -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( <PickupLocation /> );
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( <PickupLocation /> );
expect(
screen.getByText( /Collection from Local pickup/ )
).toBeInTheDocument();
} );
} );
49 changes: 27 additions & 22 deletions assets/js/base/components/cart-checkout/totals/shipping/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +36,6 @@ export interface TotalShippingProps {
className?: string;
isCheckout?: boolean;
}

export const TotalsShipping = ( {
currency,
values,
Expand All @@ -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 );
}
);
Expand Down Expand Up @@ -104,18 +111,16 @@ export const TotalsShipping = ( {
<ShippingVia
selectedShippingRates={ selectedShippingRates }
/>
{ ! prefersCollection && (
<ShippingAddress
shippingAddress={ shippingAddress }
showCalculator={ showCalculator }
isShippingCalculatorOpen={
isShippingCalculatorOpen
}
setIsShippingCalculatorOpen={
setIsShippingCalculatorOpen
}
/>
) }
<ShippingAddress
shippingAddress={ shippingAddress }
showCalculator={ showCalculator }
isShippingCalculatorOpen={
isShippingCalculatorOpen
}
setIsShippingCalculatorOpen={
setIsShippingCalculatorOpen
}
/>
</>
) : null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +22,7 @@ export interface ShippingAddressProps {
showCalculator: boolean;
isShippingCalculatorOpen: boolean;
setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ];
shippingAddress: EnteredAddress;
shippingAddress: ShippingAddressType;
}

export const ShippingAddress = ( {
Expand All @@ -30,16 +33,22 @@ 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;
}
const formattedLocation = formatShippingAddress( shippingAddress );
return (
<>
<ShippingLocation formattedLocation={ formattedLocation } />
{ showCalculator && (
{ prefersCollection ? (
<PickupLocation />
) : (
<ShippingLocation formattedLocation={ formattedLocation } />
) }
{ showCalculator && ! prefersCollection ? (
<CalculatorButton
label={ __(
'Change address',
Expand All @@ -48,7 +57,7 @@ export const ShippingAddress = ( {
isShippingCalculatorOpen={ isShippingCalculatorOpen }
setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen }
/>
) }
) : null }
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading