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

Commit

Permalink
Add "Collection from..." in Checkout sidebar when selecting local pic…
Browse files Browse the repository at this point in the history
…kup (#8305)

* Add get_collectible_method_ids function

* Add collectibleMethodIds to asset data registry

* Remove unnecessary pluck and add pickup_location to returned array

* Add hasSelectedLocalPickup to shipping types

* show shipping address even if collecting

* Make checkout store set prefersCollection based on IDs from settings

* Move areRatesCollectible outside of hook

* Add pickup location component

* Show pickup location if user prefers collection

* Move prefersCollection check into ShippingAddress component

* Remove spread for collectibleMethodIds

Not needed now since pickup_location is included in the setting by default

* Check address metadata has a value before displaying it

* Add tests for ShippingAddress component

* Move PickupLocation specific tests to new file

* Ensure TotalsShipping shows only one package rate if local pickup chosen

* Update prefersCollection selector to use typeof check

* Use isPackageRateCollectible rather than checking against settings

* Do not show calculator button if local pickup rate is selected

* Update test to mock correct setting

* Remove unused method from ShippingController

* Check isPackageRateCollectable rather than checking settings array

* Update test to mock correct setting

* Change spelling of collectible to collectable

* Improve mocked useSelect function

Old one returned incorrect data shape for prefersCollection

* Remove duplicate import
  • Loading branch information
opr authored Apr 6, 2023
1 parent b430315 commit afc9416
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 34 deletions.
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

0 comments on commit afc9416

Please sign in to comment.