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

Commit

Permalink
Fix total shipping display info when no shipping method is available (#…
Browse files Browse the repository at this point in the history
…8819)

* Fix total shipping info when no shipping are available

* Fix a logical error for displaying shipping info

* Fix failing unit tests

* Run unit test for the Cart instead of the Checkout

The calculator is only available for the Cart Block, so it doesn't make
sense to run this test for the Checkout Block

* Fix no shipping methods and incomplete address conflict

When there are no shipping methods (except for local pickup), we would
like to inform the shopper that there are no shipping options available
even though the address is complete

The solution we found is to check the address on the Cart Block only

* Refactor code

* Check whether rate is collectible without using hardcoded id

* Correctly negate hasCollectibleRate result

* Add notice when shipping is selected but no methods are available yet (#9171)

* Create useShippingTotalWarning hook

* Show notices above checkout sidebar

* Call hook to show notice in Checkout block

* Remove unused imports

* Update hook name to useShowShippingTotalWarning

* Move hook to its own file

* Import shipping data internally (without alias)

* Remove unused imports

* Move import to correct place

* Return early to avoid if else

* Refactor useShowShippingTotalWarning

* Get shipping rates directly from the cart instead of the hook

* Show shipping cost when price information is available

* Check if the passed rates are considered selected

* Prevent errors when no rates are available

---------

Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
Co-authored-by: Tarun Vijwani <tarun.vijwani@automattic.com>
  • Loading branch information
4 people authored May 25, 2023
1 parent 6f6c21c commit f317b6d
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,17 @@ const PackageRates = ( {

// Update the selected option if there is no rate selected on mount.
useEffect( () => {
if ( ! selectedOption && rates[ 0 ] ) {
setSelectedOption( rates[ 0 ].rate_id );
onSelectRate( rates[ 0 ].rate_id );
// Check the rates to see if any are marked as selected. At least one should be. If no rate is selected, it could be
// that the user toggled quickly from local pickup back to shipping.
const isRateSelectedInDataStore = rates.some(
( { selected } ) => selected
);
if (
( ! selectedOption && rates[ 0 ] ) ||
! isRateSelectedInDataStore
) {
setSelectedOption( rates[ 0 ]?.rate_id );
onSelectRate( rates[ 0 ]?.rate_id );
}
}, [ onSelectRate, rates, selectedOption ] );

Expand Down
24 changes: 16 additions & 8 deletions assets/js/base/components/cart-checkout/totals/shipping/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import ShippingCalculator from '../../shipping-calculator';
import { hasShippingRate, getTotalShippingValue } from './utils';
import {
hasShippingRate,
getTotalShippingValue,
areShippingMethodsMissing,
} from './utils';
import ShippingPlaceholder from './shipping-placeholder';
import ShippingAddress from './shipping-address';
import ShippingRateSelector from './shipping-rate-selector';
Expand Down Expand Up @@ -74,8 +78,12 @@ export const TotalsShipping = ( {
.flatMap( ( rate ) => rate.name );
}
);

const addressComplete = isAddressComplete( shippingAddress );
const shippingMethodsMissing = areShippingMethodsMissing(
hasRates,
prefersCollection,
shippingRates
);

return (
<div
Expand All @@ -87,10 +95,10 @@ export const TotalsShipping = ( {
<TotalsItem
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
value={
hasRates && cartHasCalculatedShipping
? totalShippingValue
: // if address is not complete, display the link to add an address.
! addressComplete && (
! shippingMethodsMissing && cartHasCalculatedShipping
? // if address is not complete, display the link to add an address.
totalShippingValue
: ( ! addressComplete || isCheckout ) && (
<ShippingPlaceholder
showCalculator={ showCalculator }
isCheckout={ isCheckout }
Expand All @@ -104,9 +112,9 @@ export const TotalsShipping = ( {
)
}
description={
( ! shippingMethodsMissing && cartHasCalculatedShipping ) ||
// If address is complete, display the shipping address.
( hasRates && cartHasCalculatedShipping ) ||
addressComplete ? (
( addressComplete && ! isCheckout ) ? (
<>
<ShippingVia
selectedShippingRates={ selectedShippingRates }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe( 'TotalsShipping', () => {
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ true }
isCheckout={ false }
className={ '' }
/>
</SlotFillProvider>
Expand Down Expand Up @@ -237,7 +237,7 @@ describe( 'TotalsShipping', () => {
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ true }
isCheckout={ false }
className={ '' }
/>
</SlotFillProvider>
Expand Down Expand Up @@ -282,7 +282,7 @@ describe( 'TotalsShipping', () => {
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ true }
isCheckout={ false }
className={ '' }
/>
</SlotFillProvider>
Expand Down
31 changes: 30 additions & 1 deletion assets/js/base/components/cart-checkout/totals/shipping/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { getSetting } from '@woocommerce/settings';
import type { CartResponseShippingRate } from '@woocommerce/type-defs/cart-response';
import { hasCollectableRate } from '@woocommerce/base-utils';

/**
* Searches an array of packages/rates to see if there are actually any rates
Expand All @@ -20,7 +21,7 @@ export const hasShippingRate = (
};

/**
* Calculates the total shippin value based on store settings.
* Calculates the total shipping value based on store settings.
*/
export const getTotalShippingValue = ( values: {
total_shipping: string;
Expand All @@ -31,3 +32,31 @@ export const getTotalShippingValue = ( values: {
parseInt( values.total_shipping_tax, 10 )
: parseInt( values.total_shipping, 10 );
};

/**
* Checks if no shipping methods are available or if all available shipping methods are local pickup
* only.
*/
export const areShippingMethodsMissing = (
hasRates: boolean,
prefersCollection: boolean | undefined,
shippingRates: CartResponseShippingRate[]
) => {
if ( ! hasRates ) {
// No shipping methods available
return true;
}

// We check for the availability of shipping options if the shopper selected "Shipping"
if ( ! prefersCollection ) {
return shippingRates.some(
( shippingRatePackage ) =>
! shippingRatePackage.shipping_rates.some(
( shippingRate ) =>
! hasCollectableRate( shippingRate.method_id )
)
);
}

return false;
};
1 change: 1 addition & 0 deletions assets/js/base/context/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './use-customer-data';
export * from './use-checkout-address';
export * from './use-checkout-submit';
export * from './use-checkout-extension-data';
export * from './use-show-shipping-total-warning';
export * from './use-validation';
4 changes: 4 additions & 0 deletions assets/js/base/context/hooks/shipping/use-shipping-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export const useShippingData = (): ShippingData => {
): void => {
let selectPromise;

if ( typeof newShippingRateId === 'undefined' ) {
return;
}

/**
* Picking location handling
*
Expand Down
1 change: 1 addition & 0 deletions assets/js/base/context/hooks/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"../providers/cart-checkout/checkout-events/index.tsx",
"../providers/cart-checkout/payment-events/index.tsx",
"../providers/cart-checkout/shipping/index.js",
"../../components/cart-checkout/totals/shipping",
"../../../editor-components/utils/*",
"../../../data/index.ts"
],
Expand Down
110 changes: 110 additions & 0 deletions assets/js/base/context/hooks/use-show-shipping-total-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* External dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { hasShippingRate } from '@woocommerce/base-components/cart-checkout/totals/shipping/utils';
import { hasCollectableRate } from '@woocommerce/base-utils';
import { isString } from '@woocommerce/types';

/**
* Internal dependencies
*/
import { useShippingData } from './shipping/use-shipping-data';

export const useShowShippingTotalWarning = () => {
const context = 'woocommerce/checkout-totals-block';
const errorNoticeId = 'wc-blocks-totals-shipping-warning';

const { shippingRates } = useShippingData();
const hasRates = hasShippingRate( shippingRates );
const {
prefersCollection,
isRateBeingSelected,
shippingNotices,
cartData,
} = useSelect( ( select ) => {
return {
cartData: select( CART_STORE_KEY ).getCartData(),
prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(),
isRateBeingSelected:
select( CART_STORE_KEY ).isShippingRateBeingSelected(),
shippingNotices: select( 'core/notices' ).getNotices( context ),
};
} );
const { createInfoNotice, removeNotice } = useDispatch( 'core/notices' );

useEffect( () => {
if ( ! hasRates || isRateBeingSelected ) {
// Early return because shipping rates were not yet loaded from the cart data store, or the user is changing
// rate, no need to alter the notice until we know what the actual rate is.
return;
}

const selectedRates = cartData?.shippingRates?.reduce(
( acc: string[], rate ) => {
const selectedRateForPackage = rate.shipping_rates.find(
( shippingRate ) => {
return shippingRate.selected;
}
);
if (
typeof selectedRateForPackage?.method_id !== 'undefined'
) {
acc.push( selectedRateForPackage?.method_id );
}
return acc;
},
[]
);
const isPickupRateSelected = Object.values( selectedRates ).some(
( rate: unknown ) => {
if ( isString( rate ) ) {
return hasCollectableRate( rate );
}
return false;
}
);

// There is a mismatch between the method the user chose (pickup or shipping) and the currently selected rate.
if (
hasRates &&
! prefersCollection &&
! isRateBeingSelected &&
isPickupRateSelected &&
shippingNotices.length === 0
) {
createInfoNotice(
__(
'Totals will be recalculated when a valid shipping method is selected.',
'woo-gutenberg-products-block'
),
{
id: 'wc-blocks-totals-shipping-warning',
isDismissible: false,
context,
}
);
return;
}

// Don't show the notice if they have selected local pickup, or if they have selected a valid regular shipping rate.
if (
( prefersCollection || ! isPickupRateSelected ) &&
shippingNotices.length > 0
) {
removeNotice( errorNoticeId, context );
}
}, [
cartData?.shippingRates,
createInfoNotice,
hasRates,
isRateBeingSelected,
prefersCollection,
removeNotice,
shippingNotices,
shippingRates,
] );
};
6 changes: 5 additions & 1 deletion assets/js/blocks/checkout/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { createInterpolateElement, useEffect } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
useStoreCart,
useShowShippingTotalWarning,
} from '@woocommerce/base-context/hooks';
import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
Expand Down Expand Up @@ -161,6 +164,7 @@ const Block = ( {
children: React.ReactChildren;
scrollToTop: ( props: Record< string, unknown > ) => void;
} ): JSX.Element => {
useShowShippingTotalWarning();
return (
<BlockErrorBoundary
header={ __(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
import classnames from 'classnames';
import { Icon, store, shipping } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch } from '@wordpress/data';
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { isPackageRateCollectable } from '@woocommerce/base-utils';

/**
* Internal dependencies
Expand Down Expand Up @@ -92,8 +93,17 @@ const ShippingSelector = ( {
shippingCostRequiresAddress: boolean;
toggleText: string;
} ) => {
const hasShippableRates = useSelect( ( select ) => {
const rates = select( CART_STORE_KEY ).getShippingRates();
return rates.some(
( { shipping_rates: shippingRate } ) =>
! shippingRate.every( isPackageRateCollectable )
);
} );
const rateShouldBeHidden =
shippingCostRequiresAddress && shippingAddressHasValidationErrors();
shippingCostRequiresAddress &&
shippingAddressHasValidationErrors() &&
! hasShippableRates;
const hasShippingPrices = rate.min !== undefined && rate.max !== undefined;
const { setValidationErrors, clearValidationError } =
useDispatch( VALIDATION_STORE_KEY );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import classnames from 'classnames';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';

/**
* Internal dependencies
Expand All @@ -20,6 +21,9 @@ const FrontendBlock = ( {
<Sidebar
className={ classnames( 'wc-block-checkout__sidebar', className ) }
>
<StoreNoticesContainer
context={ 'woocommerce/checkout-totals-block' }
/>
{ children }
</Sidebar>
);
Expand Down

0 comments on commit f317b6d

Please sign in to comment.