Display the link to add the shipping address when shipping address is not available (https://github.com/woocommerce/woocommerce-blocks/pull/8141)
This commit is contained in:
parent
6a629c60b2
commit
82c59ffb29
|
@ -2,56 +2,15 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { ShippingAddress, getSetting } from '@woocommerce/settings';
|
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
|
||||||
|
|
||||||
interface ShippingLocationProps {
|
interface ShippingLocationProps {
|
||||||
address: ShippingAddress;
|
formattedLocation: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Shows a formatted shipping location.
|
||||||
* Shows a formatted shipping location.
|
|
||||||
*
|
|
||||||
* @param {Object} props Incoming props for the component.
|
|
||||||
* @param {Object} props.address Incoming address information.
|
|
||||||
*/
|
|
||||||
const ShippingLocation = ( {
|
const ShippingLocation = ( {
|
||||||
address,
|
formattedLocation,
|
||||||
}: ShippingLocationProps ): JSX.Element | null => {
|
}: ShippingLocationProps ): JSX.Element | null => {
|
||||||
// we bail early if we don't have an address.
|
|
||||||
if ( Object.values( address ).length === 0 ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const shippingCountries = getSetting( 'shippingCountries', {} ) as Record<
|
|
||||||
string,
|
|
||||||
string
|
|
||||||
>;
|
|
||||||
const shippingStates = getSetting( 'shippingStates', {} ) as Record<
|
|
||||||
string,
|
|
||||||
Record< string, string >
|
|
||||||
>;
|
|
||||||
const formattedCountry =
|
|
||||||
typeof shippingCountries[ address.country ] === 'string'
|
|
||||||
? decodeEntities( shippingCountries[ address.country ] )
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const formattedState =
|
|
||||||
typeof shippingStates[ address.country ] === 'object' &&
|
|
||||||
typeof shippingStates[ address.country ][ address.state ] === 'string'
|
|
||||||
? decodeEntities(
|
|
||||||
shippingStates[ address.country ][ address.state ]
|
|
||||||
)
|
|
||||||
: address.state;
|
|
||||||
|
|
||||||
const addressParts = [];
|
|
||||||
|
|
||||||
addressParts.push( address.postcode.toUpperCase() );
|
|
||||||
addressParts.push( address.city );
|
|
||||||
addressParts.push( formattedState );
|
|
||||||
addressParts.push( formattedCountry );
|
|
||||||
|
|
||||||
const formattedLocation = addressParts.filter( Boolean ).join( ', ' );
|
|
||||||
|
|
||||||
if ( ! formattedLocation ) {
|
if ( ! formattedLocation ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,20 @@ export const CalculatorButton = ( {
|
||||||
setIsShippingCalculatorOpen,
|
setIsShippingCalculatorOpen,
|
||||||
}: CalculatorButtonProps ): JSX.Element => {
|
}: CalculatorButtonProps ): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<button
|
<a
|
||||||
className="wc-block-components-totals-shipping__change-address-button"
|
role="button"
|
||||||
onClick={ () => {
|
href="#wc-block-components-shipping-calculator-address__link"
|
||||||
|
className="wc-block-components-totals-shipping__change-address__link"
|
||||||
|
id="wc-block-components-totals-shipping__change-address__link"
|
||||||
|
onClick={ ( e ) => {
|
||||||
|
e.preventDefault();
|
||||||
setIsShippingCalculatorOpen( ! isShippingCalculatorOpen );
|
setIsShippingCalculatorOpen( ! isShippingCalculatorOpen );
|
||||||
} }
|
} }
|
||||||
|
aria-label={ label }
|
||||||
aria-expanded={ isShippingCalculatorOpen }
|
aria-expanded={ isShippingCalculatorOpen }
|
||||||
>
|
>
|
||||||
{ label }
|
{ label }
|
||||||
</button>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { Currency } from '@woocommerce/price-format';
|
||||||
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
|
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import { isAddressComplete } from '@woocommerce/base-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -67,6 +68,8 @@ export const TotalsShipping = ( {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addressComplete = isAddressComplete( shippingAddress );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ classnames(
|
className={ classnames(
|
||||||
|
@ -77,23 +80,26 @@ export const TotalsShipping = ( {
|
||||||
<TotalsItem
|
<TotalsItem
|
||||||
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
|
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
|
||||||
value={
|
value={
|
||||||
hasRates && cartHasCalculatedShipping ? (
|
hasRates && cartHasCalculatedShipping
|
||||||
totalShippingValue
|
? totalShippingValue
|
||||||
) : (
|
: // if address is not complete, display the link to add an address.
|
||||||
<ShippingPlaceholder
|
! addressComplete && (
|
||||||
showCalculator={ showCalculator }
|
<ShippingPlaceholder
|
||||||
isCheckout={ isCheckout }
|
showCalculator={ showCalculator }
|
||||||
isShippingCalculatorOpen={
|
isCheckout={ isCheckout }
|
||||||
isShippingCalculatorOpen
|
isShippingCalculatorOpen={
|
||||||
}
|
isShippingCalculatorOpen
|
||||||
setIsShippingCalculatorOpen={
|
}
|
||||||
setIsShippingCalculatorOpen
|
setIsShippingCalculatorOpen={
|
||||||
}
|
setIsShippingCalculatorOpen
|
||||||
/>
|
}
|
||||||
)
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
hasRates && cartHasCalculatedShipping ? (
|
// If address is complete, display the shipping address.
|
||||||
|
( hasRates && cartHasCalculatedShipping ) ||
|
||||||
|
addressComplete ? (
|
||||||
<>
|
<>
|
||||||
<ShippingVia
|
<ShippingVia
|
||||||
selectedShippingRates={ selectedShippingRates }
|
selectedShippingRates={ selectedShippingRates }
|
||||||
|
@ -132,6 +138,7 @@ export const TotalsShipping = ( {
|
||||||
hasRates={ hasRates }
|
hasRates={ hasRates }
|
||||||
shippingRates={ shippingRates }
|
shippingRates={ shippingRates }
|
||||||
isLoadingRates={ isLoadingRates }
|
isLoadingRates={ isLoadingRates }
|
||||||
|
isAddressComplete={ addressComplete }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { EnteredAddress } from '@woocommerce/settings';
|
import { EnteredAddress } from '@woocommerce/settings';
|
||||||
|
import {
|
||||||
|
formatShippingAddress,
|
||||||
|
isAddressComplete,
|
||||||
|
} from '@woocommerce/base-utils';
|
||||||
|
import { useEditorContext } from '@woocommerce/base-context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -23,13 +28,21 @@ export const ShippingAddress = ( {
|
||||||
setIsShippingCalculatorOpen,
|
setIsShippingCalculatorOpen,
|
||||||
shippingAddress,
|
shippingAddress,
|
||||||
}: ShippingAddressProps ): JSX.Element | null => {
|
}: ShippingAddressProps ): JSX.Element | null => {
|
||||||
|
const addressComplete = isAddressComplete( shippingAddress );
|
||||||
|
const { isEditor } = useEditorContext();
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShippingLocation address={ shippingAddress } />
|
<ShippingLocation formattedLocation={ formattedLocation } />
|
||||||
{ showCalculator && (
|
{ showCalculator && (
|
||||||
<CalculatorButton
|
<CalculatorButton
|
||||||
label={ __(
|
label={ __(
|
||||||
'(change address)',
|
'Change address',
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
||||||
|
|
|
@ -39,6 +39,10 @@ export const ShippingPlaceholder = ( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CalculatorButton
|
<CalculatorButton
|
||||||
|
label={ __(
|
||||||
|
'Add an address for shipping options',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
) }
|
||||||
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
||||||
setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen }
|
setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Notice } from 'wordpress-components';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import type { CartResponseShippingRate } from '@woocommerce/types';
|
import type { CartResponseShippingRate } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,12 +13,14 @@ export interface ShippingRateSelectorProps {
|
||||||
hasRates: boolean;
|
hasRates: boolean;
|
||||||
shippingRates: CartResponseShippingRate[];
|
shippingRates: CartResponseShippingRate[];
|
||||||
isLoadingRates: boolean;
|
isLoadingRates: boolean;
|
||||||
|
isAddressComplete: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShippingRateSelector = ( {
|
export const ShippingRateSelector = ( {
|
||||||
hasRates,
|
hasRates,
|
||||||
shippingRates,
|
shippingRates,
|
||||||
isLoadingRates,
|
isLoadingRates,
|
||||||
|
isAddressComplete,
|
||||||
}: ShippingRateSelectorProps ): JSX.Element => {
|
}: ShippingRateSelectorProps ): JSX.Element => {
|
||||||
const legend = hasRates
|
const legend = hasRates
|
||||||
? __( 'Shipping options', 'woo-gutenberg-products-block' )
|
? __( 'Shipping options', 'woo-gutenberg-products-block' )
|
||||||
|
@ -31,18 +31,13 @@ export const ShippingRateSelector = ( {
|
||||||
<ShippingRatesControl
|
<ShippingRatesControl
|
||||||
className="wc-block-components-totals-shipping__options"
|
className="wc-block-components-totals-shipping__options"
|
||||||
noResultsMessage={
|
noResultsMessage={
|
||||||
<Notice
|
<>
|
||||||
isDismissible={ false }
|
{ isAddressComplete &&
|
||||||
className={ classnames(
|
__(
|
||||||
'wc-block-components-shipping-rates-control__no-results-notice',
|
'There are no shipping options available. Please check your shipping address.',
|
||||||
'woocommerce-error'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
>
|
</>
|
||||||
{ __(
|
|
||||||
'No shipping options were found.',
|
|
||||||
'woo-gutenberg-products-block'
|
|
||||||
) }
|
|
||||||
</Notice>
|
|
||||||
}
|
}
|
||||||
shippingRates={ shippingRates }
|
shippingRates={ shippingRates }
|
||||||
isLoadingRates={ isLoadingRates }
|
isLoadingRates={ isLoadingRates }
|
||||||
|
|
|
@ -20,12 +20,16 @@
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
margin-top: ($gap-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-components-shipping-rates-control__no-results-notice {
|
.wc-block-components-shipping-rates-control__no-results-notice {
|
||||||
margin: 0 0 em($gap-small);
|
margin: 0 0 em($gap-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wc-block-components-totals-shipping__change-address__link {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
.wc-block-components-totals-shipping__change-address-button {
|
.wc-block-components-totals-shipping__change-address-button {
|
||||||
@include link-button();
|
@include link-button();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { screen, render } from '@testing-library/react';
|
||||||
|
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||||
|
import { previewCart as mockPreviewCart } from '@woocommerce/resource-previews';
|
||||||
|
import * as wpData from '@wordpress/data';
|
||||||
|
import * as baseContextHooks from '@woocommerce/base-context/hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { TotalsShipping } from '../index';
|
||||||
|
|
||||||
|
jest.mock( '@wordpress/data', () => ( {
|
||||||
|
__esModule: true,
|
||||||
|
...jest.requireActual( '@wordpress/data' ),
|
||||||
|
useSelect: jest.fn(),
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
wpData.useSelect.mockImplementation( () => {
|
||||||
|
return { prefersCollection: false };
|
||||||
|
} );
|
||||||
|
|
||||||
|
const shippingAddress = {
|
||||||
|
first_name: 'John',
|
||||||
|
last_name: 'Doe',
|
||||||
|
company: 'Company',
|
||||||
|
address_1: '409 Main Street',
|
||||||
|
address_2: 'Apt 1',
|
||||||
|
city: 'London',
|
||||||
|
postcode: 'W1T 4JG',
|
||||||
|
country: 'GB',
|
||||||
|
state: '',
|
||||||
|
email: 'john.doe@company',
|
||||||
|
phone: '+1234567890',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock( '@woocommerce/base-context/hooks', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...jest.requireActual( '@woocommerce/base-context/hooks' ),
|
||||||
|
useShippingData: jest.fn(),
|
||||||
|
useStoreCart: jest.fn(),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
baseContextHooks.useShippingData.mockReturnValue( {
|
||||||
|
needsShipping: true,
|
||||||
|
shippingRates: [
|
||||||
|
{
|
||||||
|
package_id: 0,
|
||||||
|
name: 'Shipping method',
|
||||||
|
destination: {
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
postcode: '',
|
||||||
|
country: '',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'fb0c0a746719a7596f296344b80cb2b6',
|
||||||
|
name: 'Hoodie - Blue, Yes',
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '1f0e3dad99908345f7439f8ffabdffc4',
|
||||||
|
name: 'Beanie',
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shipping_rates: [
|
||||||
|
{
|
||||||
|
rate_id: 'flat_rate:1',
|
||||||
|
name: 'Flat rate',
|
||||||
|
description: '',
|
||||||
|
delivery_time: '',
|
||||||
|
price: '500',
|
||||||
|
taxes: '0',
|
||||||
|
instance_id: 1,
|
||||||
|
method_id: 'flat_rate',
|
||||||
|
meta_data: [
|
||||||
|
{
|
||||||
|
key: 'Items',
|
||||||
|
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: false,
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
key: 'Items',
|
||||||
|
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: false,
|
||||||
|
currency_code: 'USD',
|
||||||
|
currency_symbol: '$',
|
||||||
|
currency_minor_unit: 2,
|
||||||
|
currency_decimal_separator: '.',
|
||||||
|
currency_thousand_separator: ',',
|
||||||
|
currency_prefix: '$',
|
||||||
|
currency_suffix: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rate_id: 'free_shipping:5',
|
||||||
|
name: 'Free shipping',
|
||||||
|
description: '',
|
||||||
|
delivery_time: '',
|
||||||
|
price: '0',
|
||||||
|
taxes: '0',
|
||||||
|
instance_id: 5,
|
||||||
|
method_id: 'free_shipping',
|
||||||
|
meta_data: [
|
||||||
|
{
|
||||||
|
key: 'Items',
|
||||||
|
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: true,
|
||||||
|
currency_code: 'USD',
|
||||||
|
currency_symbol: '$',
|
||||||
|
currency_minor_unit: 2,
|
||||||
|
currency_decimal_separator: '.',
|
||||||
|
currency_thousand_separator: ',',
|
||||||
|
currency_prefix: '$',
|
||||||
|
currency_suffix: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} );
|
||||||
|
baseContextHooks.useStoreCart.mockReturnValue( {
|
||||||
|
cartItems: mockPreviewCart.items,
|
||||||
|
cartTotals: [ mockPreviewCart.totals ],
|
||||||
|
cartCoupons: mockPreviewCart.coupons,
|
||||||
|
cartFees: mockPreviewCart.fees,
|
||||||
|
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||||
|
shippingRates: [],
|
||||||
|
shippingAddress,
|
||||||
|
billingAddress: mockPreviewCart.billing_address,
|
||||||
|
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||||
|
isLoadingRates: false,
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'TotalsShipping', () => {
|
||||||
|
it( 'should show correct calculator button label if address is complete', () => {
|
||||||
|
render(
|
||||||
|
<SlotFillProvider>
|
||||||
|
<TotalsShipping
|
||||||
|
currency={ {
|
||||||
|
code: 'USD',
|
||||||
|
symbol: '$',
|
||||||
|
position: 'left',
|
||||||
|
precision: 2,
|
||||||
|
} }
|
||||||
|
values={ {
|
||||||
|
total_shipping: '0',
|
||||||
|
total_shipping_tax: '0',
|
||||||
|
} }
|
||||||
|
showCalculator={ true }
|
||||||
|
showRateSelector={ true }
|
||||||
|
isCheckout={ true }
|
||||||
|
className={ '' }
|
||||||
|
/>
|
||||||
|
</SlotFillProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'Shipping to W1T 4JG, London, United Kingdom (UK)'
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect( screen.getByText( 'Change address' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
it( 'should show correct calculator button label if address is incomplete', () => {
|
||||||
|
baseContextHooks.useStoreCart.mockReturnValue( {
|
||||||
|
cartItems: mockPreviewCart.items,
|
||||||
|
cartTotals: [ mockPreviewCart.totals ],
|
||||||
|
cartCoupons: mockPreviewCart.coupons,
|
||||||
|
cartFees: mockPreviewCart.fees,
|
||||||
|
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||||
|
shippingRates: [],
|
||||||
|
shippingAddress: {
|
||||||
|
...shippingAddress,
|
||||||
|
city: '',
|
||||||
|
country: '',
|
||||||
|
postcode: '',
|
||||||
|
},
|
||||||
|
billingAddress: mockPreviewCart.billing_address,
|
||||||
|
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||||
|
isLoadingRates: false,
|
||||||
|
} );
|
||||||
|
render(
|
||||||
|
<SlotFillProvider>
|
||||||
|
<TotalsShipping
|
||||||
|
currency={ {
|
||||||
|
code: 'USD',
|
||||||
|
symbol: '$',
|
||||||
|
position: 'left',
|
||||||
|
precision: 2,
|
||||||
|
} }
|
||||||
|
values={ {
|
||||||
|
total_shipping: '0',
|
||||||
|
total_shipping_tax: '0',
|
||||||
|
} }
|
||||||
|
showCalculator={ true }
|
||||||
|
showRateSelector={ true }
|
||||||
|
isCheckout={ true }
|
||||||
|
className={ '' }
|
||||||
|
/>
|
||||||
|
</SlotFillProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByText( 'Change address' )
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText( 'Add an address for shipping options' )
|
||||||
|
).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
it( 'does not show the calculator button when default rates are available and no address has been entered', () => {
|
||||||
|
baseContextHooks.useStoreCart.mockReturnValue( {
|
||||||
|
cartItems: mockPreviewCart.items,
|
||||||
|
cartTotals: [ mockPreviewCart.totals ],
|
||||||
|
cartCoupons: mockPreviewCart.coupons,
|
||||||
|
cartFees: mockPreviewCart.fees,
|
||||||
|
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||||
|
shippingRates: mockPreviewCart.shipping_rates,
|
||||||
|
shippingAddress: {
|
||||||
|
...shippingAddress,
|
||||||
|
city: '',
|
||||||
|
country: '',
|
||||||
|
postcode: '',
|
||||||
|
},
|
||||||
|
billingAddress: mockPreviewCart.billing_address,
|
||||||
|
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||||
|
isLoadingRates: false,
|
||||||
|
} );
|
||||||
|
render(
|
||||||
|
<SlotFillProvider>
|
||||||
|
<TotalsShipping
|
||||||
|
currency={ {
|
||||||
|
code: 'USD',
|
||||||
|
symbol: '$',
|
||||||
|
position: 'left',
|
||||||
|
precision: 2,
|
||||||
|
} }
|
||||||
|
values={ {
|
||||||
|
total_shipping: '0',
|
||||||
|
total_shipping_tax: '0',
|
||||||
|
} }
|
||||||
|
showCalculator={ true }
|
||||||
|
showRateSelector={ true }
|
||||||
|
isCheckout={ true }
|
||||||
|
className={ '' }
|
||||||
|
/>
|
||||||
|
</SlotFillProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByText( 'Change address' )
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText( 'Add an address for shipping options' )
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { screen, render } from '@testing-library/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import ShippingPlaceholder from '../shipping-placeholder';
|
||||||
|
|
||||||
|
describe( 'ShippingPlaceholder', () => {
|
||||||
|
it( 'should show correct text if showCalculator is false', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<ShippingPlaceholder
|
||||||
|
showCalculator={ false }
|
||||||
|
isCheckout={ true }
|
||||||
|
isShippingCalculatorOpen={ false }
|
||||||
|
setIsShippingCalculatorOpen={ jest.fn() }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText( 'No shipping options available' )
|
||||||
|
).toBeInTheDocument();
|
||||||
|
rerender(
|
||||||
|
<ShippingPlaceholder
|
||||||
|
showCalculator={ false }
|
||||||
|
isCheckout={ false }
|
||||||
|
isShippingCalculatorOpen={ false }
|
||||||
|
setIsShippingCalculatorOpen={ jest.fn() }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText( 'Calculated during checkout' )
|
||||||
|
).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -11,7 +11,9 @@ import {
|
||||||
defaultAddressFields,
|
defaultAddressFields,
|
||||||
ShippingAddress,
|
ShippingAddress,
|
||||||
BillingAddress,
|
BillingAddress,
|
||||||
|
getSetting,
|
||||||
} from '@woocommerce/settings';
|
} from '@woocommerce/settings';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two addresses and see if they are the same.
|
* Compare two addresses and see if they are the same.
|
||||||
|
@ -100,3 +102,62 @@ export const emptyHiddenAddressFields = <
|
||||||
|
|
||||||
return newAddress;
|
return newAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Formats a shipping address for display.
|
||||||
|
*
|
||||||
|
* @param {Object} address The address to format.
|
||||||
|
* @return {string | null} The formatted address or null if no address is provided.
|
||||||
|
*/
|
||||||
|
export const formatShippingAddress = (
|
||||||
|
address: ShippingAddress | BillingAddress
|
||||||
|
): string | null => {
|
||||||
|
// We bail early if we don't have an address.
|
||||||
|
if ( Object.values( address ).length === 0 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const shippingCountries = getSetting< Record< string, string > >(
|
||||||
|
'shippingCountries',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const shippingStates = getSetting< Record< string, string > >(
|
||||||
|
'shippingStates',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const formattedCountry =
|
||||||
|
typeof shippingCountries[ address.country ] === 'string'
|
||||||
|
? decodeEntities( shippingCountries[ address.country ] )
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const formattedState =
|
||||||
|
typeof shippingStates[ address.country ] === 'object' &&
|
||||||
|
typeof shippingStates[ address.country ][ address.state ] === 'string'
|
||||||
|
? decodeEntities(
|
||||||
|
shippingStates[ address.country ][ address.state ]
|
||||||
|
)
|
||||||
|
: address.state;
|
||||||
|
|
||||||
|
const addressParts = [];
|
||||||
|
|
||||||
|
addressParts.push( address.postcode.toUpperCase() );
|
||||||
|
addressParts.push( address.city );
|
||||||
|
addressParts.push( formattedState );
|
||||||
|
addressParts.push( formattedCountry );
|
||||||
|
|
||||||
|
const formattedLocation = addressParts.filter( Boolean ).join( ', ' );
|
||||||
|
|
||||||
|
if ( ! formattedLocation ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedLocation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the address has a city and country.
|
||||||
|
*/
|
||||||
|
export const isAddressComplete = (
|
||||||
|
address: ShippingAddress | BillingAddress
|
||||||
|
): boolean => {
|
||||||
|
return !! address.city && !! address.country;
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { emptyHiddenAddressFields } from '@woocommerce/base-utils';
|
import {
|
||||||
|
emptyHiddenAddressFields,
|
||||||
|
isAddressComplete,
|
||||||
|
formatShippingAddress,
|
||||||
|
} from '@woocommerce/base-utils';
|
||||||
|
|
||||||
describe( 'emptyHiddenAddressFields', () => {
|
describe( 'emptyHiddenAddressFields', () => {
|
||||||
it( "Removes state from an address where the country doesn't use states", () => {
|
it( "Removes state from an address where the country doesn't use states", () => {
|
||||||
|
@ -22,3 +26,101 @@ describe( 'emptyHiddenAddressFields', () => {
|
||||||
expect( filteredAddress ).toHaveProperty( 'state', '' );
|
expect( filteredAddress ).toHaveProperty( 'state', '' );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
describe( 'isAddressComplete', () => {
|
||||||
|
it( 'correctly checks empty addresses', () => {
|
||||||
|
const address = {
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
company: '',
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
postcode: '',
|
||||||
|
country: '',
|
||||||
|
state: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
};
|
||||||
|
expect( isAddressComplete( address ) ).toBe( false );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'correctly checks incomplete addresses', () => {
|
||||||
|
const address = {
|
||||||
|
first_name: 'John',
|
||||||
|
last_name: 'Doe',
|
||||||
|
company: 'Company',
|
||||||
|
address_1: '409 Main Street',
|
||||||
|
address_2: 'Apt 1',
|
||||||
|
city: '',
|
||||||
|
postcode: '',
|
||||||
|
country: '',
|
||||||
|
state: '',
|
||||||
|
email: 'john.doe@company',
|
||||||
|
phone: '+1234567890',
|
||||||
|
};
|
||||||
|
expect( isAddressComplete( address ) ).toBe( false );
|
||||||
|
|
||||||
|
address.city = 'London';
|
||||||
|
expect( isAddressComplete( address ) ).toBe( false );
|
||||||
|
|
||||||
|
address.postcode = 'W1T 4JG';
|
||||||
|
address.country = 'GB';
|
||||||
|
expect( isAddressComplete( address ) ).toBe( true );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'correctly checks complete addresses', () => {
|
||||||
|
const address = {
|
||||||
|
first_name: 'John',
|
||||||
|
last_name: 'Doe',
|
||||||
|
company: 'Company',
|
||||||
|
address_1: '409 Main Street',
|
||||||
|
address_2: 'Apt 1',
|
||||||
|
city: 'London',
|
||||||
|
postcode: 'W1T 4JG',
|
||||||
|
country: 'GB',
|
||||||
|
state: '',
|
||||||
|
email: 'john.doe@company',
|
||||||
|
phone: '+1234567890',
|
||||||
|
};
|
||||||
|
expect( isAddressComplete( address ) ).toBe( true );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'formatShippingAddress', () => {
|
||||||
|
it( 'returns null if address is empty', () => {
|
||||||
|
const address = {
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
company: '',
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
postcode: '',
|
||||||
|
country: '',
|
||||||
|
state: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
};
|
||||||
|
expect( formatShippingAddress( address ) ).toBe( null );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'correctly returns the formatted address', () => {
|
||||||
|
const address = {
|
||||||
|
first_name: 'John',
|
||||||
|
last_name: 'Doe',
|
||||||
|
company: 'Company',
|
||||||
|
address_1: '409 Main Street',
|
||||||
|
address_2: 'Apt 1',
|
||||||
|
city: 'London',
|
||||||
|
postcode: 'W1T 4JG',
|
||||||
|
country: 'GB',
|
||||||
|
state: '',
|
||||||
|
email: 'john.doe@company',
|
||||||
|
phone: '+1234567890',
|
||||||
|
};
|
||||||
|
expect( formatShippingAddress( address ) ).toBe(
|
||||||
|
'W1T 4JG, London, United Kingdom (UK)'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
|
@ -26,6 +26,7 @@ const Block = ( {
|
||||||
showRateSelector={ false }
|
showRateSelector={ false }
|
||||||
values={ cartTotals }
|
values={ cartTotals }
|
||||||
currency={ totalsCurrency }
|
currency={ totalsCurrency }
|
||||||
|
isCheckout={ true }
|
||||||
/>
|
/>
|
||||||
</TotalsWrapper>
|
</TotalsWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,22 +7,21 @@ import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout
|
||||||
import {
|
import {
|
||||||
getShippingRatesPackageCount,
|
getShippingRatesPackageCount,
|
||||||
hasCollectableRate,
|
hasCollectableRate,
|
||||||
|
isAddressComplete,
|
||||||
} from '@woocommerce/base-utils';
|
} from '@woocommerce/base-utils';
|
||||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||||
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
|
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
|
||||||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { Notice } from 'wordpress-components';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import type {
|
import type {
|
||||||
PackageRateOption,
|
PackageRateOption,
|
||||||
CartShippingPackageShippingRate,
|
CartShippingPackageShippingRate,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import { useSelect } from '@wordpress/data';
|
|
||||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -87,11 +86,16 @@ const Block = ( {
|
||||||
} )
|
} )
|
||||||
: shippingRates;
|
: shippingRates;
|
||||||
|
|
||||||
|
const shippingAddress = useSelect( ( select ) => {
|
||||||
|
return select( CART_STORE_KEY ).getCustomerData()?.shippingAddress;
|
||||||
|
} );
|
||||||
|
|
||||||
if ( ! needsShipping ) {
|
if ( ! needsShipping ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shippingAddressIsComplete = ! shippingAddressHasValidationErrors();
|
const shippingAddressHasErrors = ! shippingAddressHasValidationErrors();
|
||||||
|
const addressComplete = isAddressComplete( shippingAddress );
|
||||||
|
|
||||||
const shippingRatesPackageCount =
|
const shippingRatesPackageCount =
|
||||||
getShippingRatesPackageCount( shippingRates );
|
getShippingRatesPackageCount( shippingRates );
|
||||||
|
@ -99,7 +103,7 @@ const Block = ( {
|
||||||
if (
|
if (
|
||||||
( ! hasCalculatedShipping && ! shippingRatesPackageCount ) ||
|
( ! hasCalculatedShipping && ! shippingRatesPackageCount ) ||
|
||||||
( shippingCostRequiresAddress &&
|
( shippingCostRequiresAddress &&
|
||||||
( ! shippingAddressPushed || ! shippingAddressIsComplete ) )
|
( ! shippingAddressPushed || ! shippingAddressHasErrors ) )
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
@ -121,18 +125,17 @@ const Block = ( {
|
||||||
) : (
|
) : (
|
||||||
<ShippingRatesControl
|
<ShippingRatesControl
|
||||||
noResultsMessage={
|
noResultsMessage={
|
||||||
<Notice
|
<>
|
||||||
isDismissible={ false }
|
{ addressComplete
|
||||||
className={ classnames(
|
? __(
|
||||||
'wc-block-components-shipping-rates-control__no-results-notice',
|
'There are no shipping options available. Please check your shipping address.',
|
||||||
'woocommerce-error'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
)
|
||||||
>
|
: __(
|
||||||
{ __(
|
'Add a shipping address to view shipping options.',
|
||||||
'There are no shipping options available. Please check your shipping address.',
|
'woo-gutenberg-products-block'
|
||||||
'woo-gutenberg-products-block'
|
) }
|
||||||
) }
|
</>
|
||||||
</Notice>
|
|
||||||
}
|
}
|
||||||
renderOption={ renderShippingRatesControlOption }
|
renderOption={ renderShippingRatesControlOption }
|
||||||
collapsible={ false }
|
collapsible={ false }
|
||||||
|
|
Loading…
Reference in New Issue