From 130827a2c3e31c3e527d33054a454315df5b7568 Mon Sep 17 00:00:00 2001 From: Thomas Roberts <5656702+opr@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:00:24 +0000 Subject: [PATCH] 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 --- .../js/base/context/hooks/shipping/types.ts | 4 +- .../hooks/shipping/use-shipping-data.ts | 32 ++++--- .../assets/js/base/utils/shipping-rates.js | 19 ---- .../assets/js/base/utils/shipping-rates.ts | 57 +++++++++++ .../js/base/utils/test/shipping-rates.ts | 94 +++++++++++++++++++ .../checkout-pickup-options-block/block.tsx | 3 +- .../shared/helpers.ts | 9 +- .../checkout-shipping-methods-block/block.tsx | 10 +- .../assets/js/data/checkout/selectors.ts | 9 +- .../src/Shipping/PickupLocation.php | 1 + .../src/Shipping/ShippingController.php | 27 ++++++ 11 files changed, 223 insertions(+), 42 deletions(-) delete mode 100644 plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.ts create mode 100644 plugins/woocommerce-blocks/assets/js/base/utils/test/shipping-rates.ts diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/types.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/types.ts index cd6c4de5e12..5a132d516df 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/types.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/types.ts @@ -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; } diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts index 5ea56ac57b7..c27c6dc52c5 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts @@ -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, }; }; diff --git a/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.js b/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.js deleted file mode 100644 index 54bb82f7483..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.js +++ /dev/null @@ -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 ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.ts b/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.ts new file mode 100644 index 00000000000..13a4a4036d2 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/utils/shipping-rates.ts @@ -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 ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/base/utils/test/shipping-rates.ts b/plugins/woocommerce-blocks/assets/js/base/utils/test/shipping-rates.ts new file mode 100644 index 00000000000..c28e7babeae --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/utils/test/shipping-rates.ts @@ -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 ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx index 10f2a966655..4dea0a89bdb 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx @@ -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 >( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/shared/helpers.ts b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/shared/helpers.ts index 63ba880a34f..6f71d83b5b1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/shared/helpers.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/shared/helpers.ts @@ -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 ( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index 067d17ee057..6827e06a266 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -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 + ) ), }; } ) diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts index ee64d4f9735..7cf19cb6745 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts @@ -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; }; diff --git a/plugins/woocommerce-blocks/src/Shipping/PickupLocation.php b/plugins/woocommerce-blocks/src/Shipping/PickupLocation.php index d37811e8c14..92827678c36 100644 --- a/plugins/woocommerce-blocks/src/Shipping/PickupLocation.php +++ b/plugins/woocommerce-blocks/src/Shipping/PickupLocation.php @@ -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 ); } diff --git a/plugins/woocommerce-blocks/src/Shipping/ShippingController.php b/plugins/woocommerce-blocks/src/Shipping/ShippingController.php index 18bdbae1d6b..2a20b91ea31 100644 --- a/plugins/woocommerce-blocks/src/Shipping/ShippingController.php +++ b/plugins/woocommerce-blocks/src/Shipping/ShippingController.php @@ -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. */