Allow third party methods to appear in local pickup area (https://github.com/woocommerce/woocommerce-blocks/pull/8256)

* Add get_collectible_method_ids function

* Add collectibleMethodIds to asset data registry

* Check whether method id is pickup_location/in collectibleMethodIds

* Allow selectShippingRate to be called without a package id

* Prevent collectible methods showing in the main shipping area

* Remove unnecessary pluck and add pickup_location to returned array

* No longer insert pickup_location in collectibleMethodIds

* Allow third party methods to influence low/high collection price

* Update useShippingData to consider any collectible method

* Add hasSelectedLocalPickup to shipping types

* Add dependency to selectShippingRate in useShippingData

* Register collectibleMethodIds as a callback

This is so the shipping methods get change to register before this is called. Passing a callback to `add` means it won't be called until just before it is output.

* Update supports key to 'local_pickup'

* Rename utils/shipping-rates to TS

* Convert to TS, add isPackageRateCollectible & hasCollectableRate

* Add tests for hasCollectableRate and isPackageRateCollectible

* Update shipping controller to output only method names

* Make PickupLocation shipping method support local_pickup

* Set prefersCollection based on rate ID being collectible

* Remove need to retrieve settings and use helper function instead

* rename hasCollectableRate to hasCollectibleRate

* Use array_reduce and update comments in  get_local_pickup_method_ids

* Switch order of array_unique and array_values

* Remove unneeded dependency

* Hyphenate local-pickup so it follows the same format as other features

* Update use of collectible to collectable

* Change supports feature to be hyphenated
This commit is contained in:
Thomas Roberts 2023-02-03 16:00:24 +00:00 committed by GitHub
parent 093ec68061
commit 130827a2c3
11 changed files with 223 additions and 42 deletions

View File

@ -12,10 +12,12 @@ export interface ShippingData {
// Returns a function that accepts a shipping rate ID and a package ID.
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
packageId?: string | number | undefined
) => void;
// Only true when ALL packages support local pickup. If true, we can show the collection/delivery toggle
isCollectable: boolean;
// True when a rate is currently being selected and persisted to the server.
isSelectingRate: boolean;
hasSelectedLocalPickup: boolean;
}

View File

@ -8,7 +8,10 @@ import {
import { useSelect, useDispatch } from '@wordpress/data';
import { isObject } from '@woocommerce/types';
import { useEffect, useRef, useCallback } from '@wordpress/element';
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
import {
hasCollectableRate,
deriveSelectedShippingRates,
} from '@woocommerce/base-utils';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { previewCart } from '@woocommerce/resource-previews';
@ -43,9 +46,8 @@ export const useShippingData = (): ShippingData => {
isLoadingRates: isEditor ? false : store.isCustomerDataUpdating(),
isCollectable: rates.every(
( { shipping_rates: packageShippingRates } ) =>
packageShippingRates.find(
( { method_id: methodId } ) =>
methodId === 'pickup_location'
packageShippingRates.find( ( { method_id: methodId } ) =>
hasCollectableRate( methodId )
)
),
isSelectingRate: isEditor
@ -78,6 +80,12 @@ export const useShippingData = (): ShippingData => {
) => Promise< unknown >;
};
const hasSelectedLocalPickup = hasCollectableRate(
Object.values( selectedRates.current ).map(
( rate ) => rate.split( ':' )[ 0 ]
)
);
// Selects a shipping rate, fires an event, and catch any errors.
const { dispatchCheckoutEvent } = useStoreEvents();
const selectShippingRate = useCallback(
@ -92,12 +100,8 @@ export const useShippingData = (): ShippingData => {
*
* Forces pickup location to be selected for all packages since we don't allow a mix of shipping and pickup.
*/
const hasSelectedLocalPickup = !! Object.values(
selectedRates.current
).find( ( rate ) => rate.includes( 'pickup_location:' ) );
if (
newShippingRateId.includes( 'pickup_location:' ) ||
hasCollectableRate( newShippingRateId.split( ':' )[ 0 ] ) ||
hasSelectedLocalPickup
) {
selectPromise = dispatchSelectShippingRate( newShippingRateId );
@ -117,7 +121,11 @@ export const useShippingData = (): ShippingData => {
processErrorResponse( error );
} );
},
[ dispatchSelectShippingRate, dispatchCheckoutEvent, selectedRates ]
[
hasSelectedLocalPickup,
dispatchSelectShippingRate,
dispatchCheckoutEvent,
]
);
return {
@ -129,8 +137,6 @@ export const useShippingData = (): ShippingData => {
hasCalculatedShipping,
isLoadingRates,
isCollectable,
hasSelectedLocalPickup: !! Object.values( selectedRates.current ).find(
( rate ) => rate.includes( 'pickup_location:' )
),
hasSelectedLocalPickup,
};
};

View File

@ -1,19 +0,0 @@
/**
* Get the number of packages in a shippingRates array.
*
* @param {Array} shippingRates Shipping rates and packages array.
*/
export const getShippingRatesPackageCount = ( shippingRates ) => {
return shippingRates.length;
};
/**
* Get the number of rates in a shippingRates array.
*
* @param {Array} shippingRates Shipping rates and packages array.
*/
export const getShippingRatesRateCount = ( shippingRates ) => {
return shippingRates.reduce( function ( count, shippingPackage ) {
return count + shippingPackage.shipping_rates.length;
}, 0 );
};

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import {
CartShippingPackageShippingRate,
CartShippingRate,
} from '@woocommerce/type-defs/cart';
import { getSetting } from '@woocommerce/settings';
/**
* Get the number of packages in a shippingRates array.
*
* @param {Array} shippingRates Shipping rates and packages array.
*/
export const getShippingRatesPackageCount = (
shippingRates: CartShippingRate[]
) => {
return shippingRates.length;
};
const collectableMethodIds = getSetting< string[] >(
'collectableMethodIds',
[]
);
/**
* If the package rate's method_id is in the collectableMethodIds array, return true.
*/
export const isPackageRateCollectable = (
rate: CartShippingPackageShippingRate
): boolean => collectableMethodIds.includes( rate.method_id );
/**
* Check if the specified rates are collectable. Accepts either an array of rate names, or a single string.
*/
export const hasCollectableRate = (
chosenRates: string[] | string
): boolean => {
if ( Array.isArray( chosenRates ) ) {
return !! chosenRates.find( ( rate ) =>
collectableMethodIds.includes( rate )
);
}
return collectableMethodIds.includes( chosenRates );
};
/**
* Get the number of rates in a shippingRates array.
*
* @param {Array} shippingRates Shipping rates and packages array.
*/
export const getShippingRatesRateCount = (
shippingRates: CartShippingRate[]
) => {
return shippingRates.reduce( function ( count, shippingPackage ) {
return count + shippingPackage.shipping_rates.length;
}, 0 );
};

View File

@ -0,0 +1,94 @@
/**
* External dependencies
*/
import {
hasCollectableRate,
isPackageRateCollectable,
} from '@woocommerce/base-utils';
import { CartShippingRate } from '@woocommerce/type-defs/cart';
jest.mock( '@woocommerce/settings', () => {
return {
...jest.requireActual( '@woocommerce/settings' ),
getSetting: ( setting: string ) => {
if ( setting === 'collectableMethodIds' ) {
return [ 'local_pickup' ];
}
return jest
.requireActual( '@woocommerce/settings' )
.getSetting( setting );
},
};
} );
describe( 'hasCollectableRate', () => {
it( 'correctly identifies if an array contains a collectable rate', () => {
const ratesToTest = [ 'flat_rate', 'local_pickup' ];
expect( hasCollectableRate( ratesToTest ) ).toBe( true );
const ratesToTest2 = [ 'flat_rate', 'free_shipping' ];
expect( hasCollectableRate( ratesToTest2 ) ).toBe( false );
} );
} );
describe( 'isPackageRateCollectable', () => {
it( 'correctly identifies if a package rate is collectable or not', () => {
const testPackage: CartShippingRate = {
package_id: 0,
name: 'Shipping',
destination: {
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
items: [],
shipping_rates: [
{
rate_id: 'flat_rate:1',
name: 'Flat rate',
description: '',
delivery_time: '',
price: '10',
taxes: '0',
instance_id: 1,
method_id: 'flat_rate',
meta_data: [],
selected: true,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
{
rate_id: 'local_pickup:2',
name: 'Local pickup',
description: '',
delivery_time: '',
price: '0',
taxes: '0',
instance_id: 2,
method_id: 'local_pickup',
meta_data: [],
selected: false,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
],
};
expect(
isPackageRateCollectable( testPackage.shipping_rates[ 0 ] )
).toBe( false );
expect(
isPackageRateCollectable( testPackage.shipping_rates[ 1 ] )
).toBe( true );
} );
} );

View File

@ -17,6 +17,7 @@ import { Icon, mapMarker } from '@wordpress/icons';
import RadioControl from '@woocommerce/base-components/radio-control';
import type { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
import { CartShippingPackageShippingRate } from '@woocommerce/types';
import { isPackageRateCollectable } from '@woocommerce/base-utils';
/**
* Internal dependencies
@ -118,7 +119,7 @@ const Block = (): JSX.Element | null => {
// Get pickup locations from the first shipping package.
const pickupLocations = ( shippingRates[ 0 ]?.shipping_rates || [] ).filter(
( { method_id: methodId } ) => methodId === 'pickup_location'
isPackageRateCollectable
);
const [ selectedOption, setSelectedOption ] = useState< string >(

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import type { CartShippingPackageShippingRate } from '@woocommerce/type-defs/cart';
import { hasCollectableRate } from '@woocommerce/base-utils';
export interface minMaxPrices {
min: CartShippingPackageShippingRate | undefined;
@ -25,7 +26,7 @@ export function getShippingPrices(
lowestRate: CartShippingPackageShippingRate | undefined,
currentRate: CartShippingPackageShippingRate
) => {
if ( currentRate.method_id === 'pickup_location' ) {
if ( hasCollectableRate( currentRate.method_id ) ) {
return lowestRate;
}
if (
@ -44,7 +45,7 @@ export function getShippingPrices(
highestRate: CartShippingPackageShippingRate | undefined,
currentRate: CartShippingPackageShippingRate
) => {
if ( currentRate.method_id === 'pickup_location' ) {
if ( hasCollectableRate( currentRate.method_id ) ) {
return highestRate;
}
if (
@ -83,7 +84,7 @@ export function getLocalPickupPrices(
lowestRate: CartShippingPackageShippingRate | undefined,
currentRate: CartShippingPackageShippingRate
) => {
if ( currentRate.method_id !== 'pickup_location' ) {
if ( hasCollectableRate( currentRate.method_id ) ) {
return lowestRate;
}
if (
@ -101,7 +102,7 @@ export function getLocalPickupPrices(
highestRate: CartShippingPackageShippingRate | undefined,
currentRate: CartShippingPackageShippingRate
) => {
if ( currentRate.method_id !== 'pickup_location' ) {
if ( hasCollectableRate( currentRate.method_id ) ) {
return highestRate;
}
if (

View File

@ -4,7 +4,10 @@
import { __ } from '@wordpress/i18n';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
import { getShippingRatesPackageCount } from '@woocommerce/base-utils';
import {
getShippingRatesPackageCount,
hasCollectableRate,
} from '@woocommerce/base-utils';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
@ -66,8 +69,9 @@ const Block = (): JSX.Element | null => {
...shippingRatesPackage,
shipping_rates: shippingRatesPackage.shipping_rates.filter(
( shippingRatesPackageRate ) =>
shippingRatesPackageRate.method_id !==
'pickup_location'
! hasCollectableRate(
shippingRatesPackageRate.method_id
)
),
};
} )

View File

@ -2,6 +2,8 @@
* External dependencies
*/
import { select } from '@wordpress/data';
import { hasCollectableRate } from '@woocommerce/base-utils';
import { isString, objectHasProp } from '@woocommerce/types';
/**
* Internal dependencies
@ -83,7 +85,12 @@ export const prefersCollection = ( state: CheckoutState ) => {
const selectedRate = shippingRates[ 0 ].shipping_rates.find(
( rate ) => rate.selected
);
return selectedRate?.method_id === 'pickup_location';
if (
objectHasProp( selectedRate, 'method_id' ) &&
isString( selectedRate.method_id )
) {
return hasCollectableRate( selectedRate?.method_id );
}
}
return state.prefersCollection;
};

View File

@ -26,6 +26,7 @@ class PickupLocation extends WC_Shipping_Method {
$this->title = $this->get_option( 'title' );
$this->tax_status = $this->get_option( 'tax_status' );
$this->cost = $this->get_option( 'cost' );
$this->supports = [ 'local-pickup' ];
$this->pickup_locations = get_option( $this->id . '_pickup_locations', [] );
add_filter( 'woocommerce_attribute_label', array( $this, 'translate_meta_data' ), 10, 3 );
}

View File

@ -49,6 +49,7 @@ class ShippingController {
true
);
}
$this->asset_data_registry->add( 'collectableMethodIds', array( $this, 'get_local_pickup_method_ids' ), true );
add_action( 'rest_api_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'hydrate_client_settings' ] );
@ -60,6 +61,32 @@ class ShippingController {
add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) );
}
/**
* Gets a list of payment method ids that support the 'local-pickup' feature.
*
* @return string[] List of payment method ids that support the 'local-pickup' feature.
*/
public function get_local_pickup_method_ids() {
$all_methods_supporting_local_pickup = array_reduce(
WC()->shipping()->get_shipping_methods(),
function( $methods, $method ) {
if ( $method->supports( 'local-pickup' ) ) {
$methods[] = $method->id;
}
return $methods;
},
array()
);
// We use array_values because this will be used in JS, so we don't need the (numerical) keys.
return array_values(
// This array_unique is necessary because WC()->shipping()->get_shipping_methods() can return duplicates.
array_unique(
$all_methods_supporting_local_pickup
)
);
}
/**
* Register Local Pickup settings for rest api.
*/