Update and select shipping rates dynamically (https://github.com/woocommerce/woocommerce-blocks/pull/1794)
* add select shipping endpoint to router * add select shipping method * add selected rates to cart * better select rates * move schema function to seperate function * move validation to Cart Controller * fix wrong session key * Update shipping/cart endpoints (https://github.com/woocommerce/woocommerce-blocks/pull/1833) * Items should not have keys in API response * Include package ID in response (this is just a basic index) * /cart/select-shipping-rate/package_id * Add package_id to package array * Update responses and add shipping-rates to main cart endpoint * update-shipping endpoint * Add querying selected shipping rate to the store (https://github.com/woocommerce/woocommerce-blocks/pull/1829) * add selecting shipping to store * directly call useSelectShippingRate * refactor cart keys transformation to reducer * remove selecting first result and accept selecting * move update shipping to new endpoint * pass selected rates down * select shipping right directly and fix editor issues * fix some broken prop types * key -> package id * Update and fix cart/shipping-rate tests * fix case for when rates are set * Update useShippingRates test * add args to rest endpoint * move selecting shipping rate logic to hook * fix some naming issues * update propTypes * update action call * fully watch cart state * address review issues * fix prop type issues * fix issue with rates not loading in checkout * remove extra package for shipping * move ShippingCalculatorOptions to outside Co-authored-by: Mike Jolley <mike.jolley@me.com> Co-authored-by: Albert Juhé Lluveras <aljullu@gmail.com>
This commit is contained in:
parent
5a26d2708e
commit
2c8388f0a8
|
@ -26,7 +26,10 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
|||
<Button
|
||||
className="wc-block-shipping-calculator-address__button"
|
||||
disabled={ isShallowEqual( address, initialAddress ) }
|
||||
onClick={ () => onUpdate( address ) }
|
||||
onClick={ ( e ) => {
|
||||
e.preventDefault();
|
||||
return onUpdate( address );
|
||||
} }
|
||||
type="submit"
|
||||
>
|
||||
{ __( 'Update', 'woo-gutenberg-products-block' ) }
|
||||
|
|
|
@ -13,6 +13,10 @@ import { decodeEntities } from '@wordpress/html-entities';
|
|||
* Shows a formatted shipping location.
|
||||
*/
|
||||
const ShippingLocation = ( { address } ) => {
|
||||
// we bail early if we don't have an address.
|
||||
if ( Object.values( address ).length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
const formattedCountry =
|
||||
typeof SHIPPING_COUNTRIES[ address.country ] === 'string'
|
||||
? decodeEntities( SHIPPING_COUNTRIES[ address.country ] )
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePrevious, useShippingRates } from '@woocommerce/base-hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { usePrevious } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -14,55 +13,17 @@ import LoadingMask from '../loading-mask';
|
|||
import './style.scss';
|
||||
|
||||
const ShippingRatesControl = ( {
|
||||
address,
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
className,
|
||||
noResultsMessage,
|
||||
onChange,
|
||||
renderOption,
|
||||
selected = [],
|
||||
} ) => {
|
||||
const { shippingRates, shippingRatesLoading } = useShippingRates( address );
|
||||
const previousShippingRates = usePrevious(
|
||||
shippingRates,
|
||||
( newRates ) => newRates.length > 0
|
||||
);
|
||||
|
||||
// Select first item when shipping rates are loaded.
|
||||
useEffect(
|
||||
() => {
|
||||
if ( shippingRates.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelectedValid =
|
||||
selected.length === shippingRates.length &&
|
||||
selected.every( ( selectedId, i ) => {
|
||||
const rates = shippingRates[ i ].shipping_rates;
|
||||
return rates.some(
|
||||
( { rate_id: rateId } ) => rateId === selectedId
|
||||
);
|
||||
} );
|
||||
|
||||
if ( isSelectedValid ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newShippingRates = shippingRates.map( ( shippingRate ) => {
|
||||
if ( shippingRate.shipping_rates.length > 0 ) {
|
||||
return shippingRate.shipping_rates[ 0 ].rate_id;
|
||||
}
|
||||
return null;
|
||||
} );
|
||||
|
||||
if ( newShippingRates.length > 0 ) {
|
||||
onChange( newShippingRates );
|
||||
}
|
||||
},
|
||||
// We only want to run this when `shippingRates` changes,
|
||||
// so there is no need to add `selected` to the effect dependencies.
|
||||
[ shippingRates ]
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingMask
|
||||
isLoading={ shippingRatesLoading }
|
||||
|
@ -75,9 +36,7 @@ const ShippingRatesControl = ( {
|
|||
<Packages
|
||||
className={ className }
|
||||
noResultsMessage={ noResultsMessage }
|
||||
onChange={ onChange }
|
||||
renderOption={ renderOption }
|
||||
selected={ selected }
|
||||
shippingRates={ previousShippingRates || shippingRates }
|
||||
/>
|
||||
</LoadingMask>
|
||||
|
@ -94,10 +53,8 @@ ShippingRatesControl.propTypes = {
|
|||
country: PropTypes.string,
|
||||
} ),
|
||||
noResultsMessage: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
renderOption: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
selected: PropTypes.arrayOf( PropTypes.string ),
|
||||
};
|
||||
|
||||
export default ShippingRatesControl;
|
||||
|
|
|
@ -81,9 +81,10 @@ Package.propTypes = {
|
|||
renderOption: PropTypes.func.isRequired,
|
||||
shippingRate: PropTypes.shape( {
|
||||
shipping_rates: PropTypes.arrayOf( PropTypes.object ).isRequired,
|
||||
items: PropTypes.objectOf(
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
name: PropTypes.string.isRequired,
|
||||
key: PropTypes.string.isRequired,
|
||||
quantity: PropTypes.number.isRequired,
|
||||
} ).isRequired
|
||||
).isRequired,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelectShippingRate } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -12,23 +13,22 @@ import './style.scss';
|
|||
const Packages = ( {
|
||||
className,
|
||||
noResultsMessage,
|
||||
onChange = () => {},
|
||||
renderOption,
|
||||
selected = [],
|
||||
shippingRates,
|
||||
shippingRates = [],
|
||||
} ) => {
|
||||
const { selectShippingRate, selectedShippingRates } = useSelectShippingRate(
|
||||
shippingRates
|
||||
);
|
||||
return shippingRates.map( ( shippingRate, i ) => (
|
||||
<Package
|
||||
key={ Object.keys( shippingRate.items ).join() }
|
||||
key={ shippingRate.package_id }
|
||||
className={ className }
|
||||
noResultsMessage={ noResultsMessage }
|
||||
onChange={ ( newShippingRate ) => {
|
||||
const newSelected = [ ...selected ];
|
||||
newSelected[ i ] = newShippingRate;
|
||||
onChange( newSelected );
|
||||
selectShippingRate( newShippingRate, i );
|
||||
} }
|
||||
renderOption={ renderOption }
|
||||
selected={ selected[ i ] }
|
||||
selected={ selectedShippingRates[ i ] }
|
||||
shippingRate={ shippingRate }
|
||||
showItems={ shippingRates.length > 1 }
|
||||
title={ shippingRates.length > 1 ? shippingRate.name : null }
|
||||
|
@ -40,11 +40,19 @@ Packages.propTypes = {
|
|||
renderOption: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
noResultsMessage: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
selected: PropTypes.arrayOf( PropTypes.string ),
|
||||
shippingRates: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
items: PropTypes.object.isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
quantity: PropTypes.number,
|
||||
} )
|
||||
).isRequired,
|
||||
package_id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
destination: PropTypes.object,
|
||||
shipping_rates: PropTypes.arrayOf( PropTypes.object ),
|
||||
} ).isRequired
|
||||
).isRequired,
|
||||
};
|
||||
|
|
|
@ -12,3 +12,4 @@ export * from './use-previous';
|
|||
export * from './checkout';
|
||||
export * from './payment-methods';
|
||||
export * from './use-shipping-rates';
|
||||
export * from './use-select-shipping-rate';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -12,12 +12,7 @@ import { useShippingRates } from '../use-shipping-rates';
|
|||
|
||||
jest.mock( '@woocommerce/block-data', () => ( {
|
||||
__esModule: true,
|
||||
COLLECTIONS_STORE_KEY: 'test/store',
|
||||
} ) );
|
||||
|
||||
// Make debounce instantaneous.
|
||||
jest.mock( 'use-debounce', () => ( {
|
||||
useDebounce: ( a ) => [ a ],
|
||||
CART_STORE_KEY: 'test/store',
|
||||
} ) );
|
||||
|
||||
describe( 'useShippingRates', () => {
|
||||
|
@ -33,24 +28,33 @@ describe( 'useShippingRates', () => {
|
|||
};
|
||||
};
|
||||
|
||||
const getWrappedComponents = ( Component, props ) => (
|
||||
const getWrappedComponents = ( Component ) => (
|
||||
<RegistryProvider value={ registry }>
|
||||
<Component { ...props } />
|
||||
<Component />
|
||||
</RegistryProvider>
|
||||
);
|
||||
|
||||
const getTestComponent = () => ( { query } ) => {
|
||||
const items = useShippingRates( query );
|
||||
const getTestComponent = () => () => {
|
||||
const items = useShippingRates();
|
||||
return <div { ...items } />;
|
||||
};
|
||||
|
||||
const mockCartData = {
|
||||
coupons: [],
|
||||
items: [ { foo: 'bar' } ],
|
||||
itemsCount: 123,
|
||||
itemsWeight: 123,
|
||||
needsShipping: false,
|
||||
shippingRates: { foo: 'bar' },
|
||||
};
|
||||
|
||||
const setUpMocks = () => {
|
||||
mocks = {
|
||||
selectors: {
|
||||
getCollectionError: jest.fn().mockReturnValue( false ),
|
||||
getCollection: jest
|
||||
.fn()
|
||||
.mockImplementation( () => ( { foo: 'bar' } ) ),
|
||||
getCartData: jest.fn().mockReturnValue( mockCartData ),
|
||||
getCartErrors: jest.fn().mockReturnValue( false ),
|
||||
getCartTotals: jest.fn().mockReturnValue( 123 ),
|
||||
areShippingRatesLoading: jest.fn().mockReturnValue( false ),
|
||||
hasFinishedResolution: jest.fn().mockReturnValue( true ),
|
||||
},
|
||||
};
|
||||
|
@ -66,49 +70,22 @@ describe( 'useShippingRates', () => {
|
|||
renderer = null;
|
||||
setUpMocks();
|
||||
} );
|
||||
it(
|
||||
'should return expected behaviour for equivalent query on props ' +
|
||||
'across renders',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { bar: 'foo' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { shippingRates } = getProps( renderer );
|
||||
// rerender
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { bar: 'foo' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
// re-render should result in same shippingRates object because although
|
||||
// query-state is a different instance, it's still equivalent.
|
||||
const { shippingRates: newShippingRates } = getProps( renderer );
|
||||
expect( newShippingRates ).toBe( shippingRates );
|
||||
// now let's change the query passed through to verify new object
|
||||
// is created.
|
||||
// remember this won't actually change the results because the mock
|
||||
// selector is returning an equivalent object when it is called,
|
||||
// however it SHOULD be a new object instance.
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { foo: 'bar' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { shippingRates: shippingRatesVerification } = getProps(
|
||||
renderer
|
||||
it( 'should return expected shipping rates provided by the store', () => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent )
|
||||
);
|
||||
expect( shippingRatesVerification ).not.toBe( shippingRates );
|
||||
expect( shippingRatesVerification ).toEqual( shippingRates );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
} );
|
||||
const { shippingRates } = getProps( renderer );
|
||||
expect( shippingRates ).toBe( mockCartData.shippingRates );
|
||||
// rerender
|
||||
act( () => {
|
||||
renderer.update( getWrappedComponents( TestComponent ) );
|
||||
} );
|
||||
// re-render should result in same shippingRates object.
|
||||
const { shippingRates: newShippingRates } = getProps( renderer );
|
||||
expect( newShippingRates ).toBe( shippingRates );
|
||||
renderer.unmount();
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -18,24 +18,24 @@ import { useShallowEqual } from './use-shallow-equal';
|
|||
* @throws {Object} Throws an exception object if there was a problem with the
|
||||
* API request, to be picked up by BlockErrorBoundry.
|
||||
*
|
||||
* @param {Object} options An object declaring the various
|
||||
* collection arguments.
|
||||
* @param {string} options.namespace The namespace for the collection.
|
||||
* Example: `'/wc/blocks'`
|
||||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {Array} options.resourceValues An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
* @param {Object} options.query An object of key value pairs for the
|
||||
* query to execute on the collection
|
||||
* (optional). Example:
|
||||
* `{ order: 'ASC', order_by: 'price' }`
|
||||
* @param {boolean} options.shouldSelect If false, the previous results will be
|
||||
* returned and internal selects will not
|
||||
* fire.
|
||||
* @param {Object} options An object declaring the various
|
||||
* collection arguments.
|
||||
* @param {string} options.namespace The namespace for the collection.
|
||||
* Example: `'/wc/blocks'`
|
||||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {Array} [options.resourceValues] An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
* @param {Object} [options.query] An object of key value pairs for the
|
||||
* query to execute on the collection
|
||||
* Example:
|
||||
* `{ order: 'ASC', order_by: 'price' }`
|
||||
* @param {boolean} [options.shouldSelect] If false, the previous results will be
|
||||
* returned and internal selects will not
|
||||
* fire.
|
||||
*
|
||||
* @return {Object} This hook will return an object with two properties:
|
||||
* - results An array of collection items returned.
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* This is a custom hook for loading the selected shipping rate from the cart store and actions for selecting a rate.
|
||||
See also: https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/master/src/RestApi/StoreApi
|
||||
*
|
||||
* @param {Array} shippingRates an array of packages with shipping rates.
|
||||
* @return {Object} This hook will return an object with two properties:
|
||||
* - selectShippingRate A function that immediately returns the selected
|
||||
* rate and dispatches an action generator.
|
||||
* - selectedShippingRates An array of selected shipping rates, maintained
|
||||
* locally by a state and updated optimistically.
|
||||
*/
|
||||
export const useSelectShippingRate = ( shippingRates ) => {
|
||||
const initiallySelectedRates = shippingRates.map(
|
||||
// the API responds with those keys.
|
||||
// eslint-disable-next-line camelcase
|
||||
( p ) => p.shipping_rates.find( ( rate ) => rate.selected )?.rate_id
|
||||
);
|
||||
const [ selectedShippingRates, setSelectedShipping ] = useState(
|
||||
initiallySelectedRates
|
||||
);
|
||||
const { selectShippingRate } = useDispatch( storeKey );
|
||||
const setRate = ( newShippingRate, packageId ) => {
|
||||
setSelectedShipping( [ newShippingRate ] );
|
||||
selectShippingRate( newShippingRate, packageId );
|
||||
};
|
||||
return {
|
||||
selectShippingRate: setRate,
|
||||
selectedShippingRates,
|
||||
};
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCollection } from './use-collection';
|
||||
import { useStoreCart } from './use-store-cart';
|
||||
|
||||
/**
|
||||
* This is a custom hook that is wired up to the `wc/store/collections` data
|
||||
|
@ -14,29 +14,27 @@ import { useCollection } from './use-collection';
|
|||
* will ensure a component is kept up to date with the shipping rates matching that
|
||||
* query in the store state.
|
||||
*
|
||||
* @param {Object} query An object containing any query arguments to be
|
||||
* included with the collection request for the
|
||||
* shipping rates. Does not have to be included.
|
||||
*
|
||||
* @return {Object} This hook will return an object with three properties:
|
||||
* - shippingRates An array of shipping rate objects.
|
||||
* - shippingRatesLoading A boolean indicating whether the shipping
|
||||
* rates are still loading or not.
|
||||
* - updateShipping An action dispatcher to update the shipping address.
|
||||
*/
|
||||
export const useShippingRates = ( query ) => {
|
||||
const [ debouncedQuery ] = useDebounce( query, 300 );
|
||||
export const useShippingRates = () => {
|
||||
const { shippingRates } = useStoreCart();
|
||||
const results = useSelect( ( select, { dispatch } ) => {
|
||||
const store = select( storeKey );
|
||||
const shippingRatesLoading = store.areShippingRatesLoading();
|
||||
const { updateShippingAddress } = dispatch( storeKey );
|
||||
|
||||
const {
|
||||
results: shippingRates,
|
||||
isLoading: shippingRatesLoading,
|
||||
} = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'cart/shipping-rates',
|
||||
query: debouncedQuery,
|
||||
} );
|
||||
return {
|
||||
shippingRatesLoading,
|
||||
updateShippingAddress,
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return {
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
...results,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -51,6 +51,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
|
||||
return {
|
||||
cartCoupons: cartData.coupons,
|
||||
shippingRates: cartData.shippingRates,
|
||||
cartItems: cartData.items,
|
||||
cartItemsCount: cartData.itemsCount,
|
||||
cartItemsWeight: cartData.itemsWeight,
|
||||
|
|
|
@ -29,6 +29,7 @@ const CartFrontend = ( {
|
|||
cartTotals,
|
||||
cartIsLoading,
|
||||
cartCoupons,
|
||||
shippingRates,
|
||||
} = useStoreCart();
|
||||
|
||||
return (
|
||||
|
@ -46,6 +47,7 @@ const CartFrontend = ( {
|
|||
}
|
||||
isShippingCostHidden={ isShippingCostHidden }
|
||||
isLoading={ cartIsLoading }
|
||||
shippingRates={ shippingRates }
|
||||
/>
|
||||
</LoadingMask>
|
||||
) }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
@ -8,9 +9,7 @@ import {
|
|||
TotalsCouponCodeInput,
|
||||
TotalsItem,
|
||||
} from '@woocommerce/base-components/totals';
|
||||
import ShippingRatesControl, {
|
||||
Packages,
|
||||
} from '@woocommerce/base-components/shipping-rates-control';
|
||||
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
|
||||
import ShippingCalculator from '@woocommerce/base-components/shipping-calculator';
|
||||
import ShippingLocation from '@woocommerce/base-components/shipping-location';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
|
@ -24,7 +23,7 @@ import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
|
|||
import { Card, CardBody } from 'wordpress-components';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useStoreCartCoupons } from '@woocommerce/base-hooks';
|
||||
import { useStoreCartCoupons, useShippingRates } from '@woocommerce/base-hooks';
|
||||
import classnames from 'classnames';
|
||||
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
|
||||
|
||||
|
@ -55,6 +54,43 @@ const renderShippingRatesControlOption = ( option ) => ( {
|
|||
),
|
||||
} );
|
||||
|
||||
const ShippingCalculatorOptions = ( {
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
shippingAddress,
|
||||
} ) => {
|
||||
return (
|
||||
<fieldset className="wc-block-cart__shipping-options-fieldset">
|
||||
<legend className="screen-reader-text">
|
||||
{ __(
|
||||
'Choose the shipping method.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</legend>
|
||||
<ShippingRatesControl
|
||||
className="wc-block-cart__shipping-options"
|
||||
address={
|
||||
shippingAddress
|
||||
? {
|
||||
city: shippingAddress.city,
|
||||
state: shippingAddress.state,
|
||||
postcode: shippingAddress.postcode,
|
||||
country: shippingAddress.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'No shipping options were found.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
renderOption={ renderShippingRatesControlOption }
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={ shippingRatesLoading }
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that renders the Cart block when user has something in cart aka "full".
|
||||
*/
|
||||
|
@ -67,16 +103,8 @@ const Cart = ( {
|
|||
shippingRates,
|
||||
isLoading = false,
|
||||
} ) => {
|
||||
const [ selectedShippingRate, setSelectedShippingRate ] = useState();
|
||||
const [
|
||||
shippingCalculatorAddress,
|
||||
setShippingCalculatorAddress,
|
||||
] = useState( {
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
} );
|
||||
const { updateShippingAddress, shippingRatesLoading } = useShippingRates();
|
||||
const shippingAddress = shippingRates[ 0 ]?.destination;
|
||||
const [ showShippingCosts, setShowShippingCosts ] = useState(
|
||||
! isShippingCostHidden
|
||||
);
|
||||
|
@ -93,7 +121,7 @@ const Cart = ( {
|
|||
}
|
||||
if ( isShippingCalculatorEnabled ) {
|
||||
if ( isShippingCostHidden ) {
|
||||
if ( shippingCalculatorAddress.country ) {
|
||||
if ( shippingAddress?.country ) {
|
||||
return setShowShippingCosts( true );
|
||||
}
|
||||
} else {
|
||||
|
@ -101,11 +129,7 @@ const Cart = ( {
|
|||
}
|
||||
}
|
||||
return setShowShippingCosts( false );
|
||||
}, [
|
||||
isShippingCalculatorEnabled,
|
||||
isShippingCostHidden,
|
||||
shippingCalculatorAddress,
|
||||
] );
|
||||
}, [ isShippingCalculatorEnabled, isShippingCostHidden, shippingAddress ] );
|
||||
|
||||
/**
|
||||
* Given an API response with cart totals, generates an array of rows to display in the Cart block.
|
||||
|
@ -196,12 +220,10 @@ const Cart = ( {
|
|||
: totalShipping,
|
||||
description: (
|
||||
<>
|
||||
<ShippingLocation
|
||||
address={ shippingCalculatorAddress }
|
||||
/>
|
||||
<ShippingLocation address={ shippingAddress } />
|
||||
<ShippingCalculator
|
||||
address={ shippingCalculatorAddress }
|
||||
setAddress={ setShippingCalculatorAddress }
|
||||
address={ shippingAddress }
|
||||
setAddress={ updateShippingAddress }
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
@ -254,56 +276,13 @@ const Cart = ( {
|
|||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</legend>
|
||||
{ shippingRates ? (
|
||||
<Packages
|
||||
className="wc-block-cart__shipping-options"
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
selected={ [
|
||||
// This is only rendered in the editor, with placeholder
|
||||
// shippingRates, so we can safely fallback to set the
|
||||
// first shipping rate as selected and ignore setting
|
||||
// an onChange prop.
|
||||
shippingRates[ 0 ]
|
||||
.shipping_rates[ 0 ].rate_id,
|
||||
] }
|
||||
/>
|
||||
) : (
|
||||
<ShippingRatesControl
|
||||
className="wc-block-cart__shipping-options"
|
||||
address={
|
||||
shippingCalculatorAddress.country
|
||||
? {
|
||||
city:
|
||||
shippingCalculatorAddress.city,
|
||||
state:
|
||||
shippingCalculatorAddress.state,
|
||||
postcode:
|
||||
shippingCalculatorAddress.postcode,
|
||||
country:
|
||||
shippingCalculatorAddress.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'No shipping options were found.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
selected={ selectedShippingRate }
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
onChange={ (
|
||||
newSelectedShippingOption
|
||||
) =>
|
||||
setSelectedShippingRate(
|
||||
newSelectedShippingOption
|
||||
)
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<ShippingCalculatorOptions
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={
|
||||
shippingRatesLoading
|
||||
}
|
||||
shippingAddress={ shippingAddress }
|
||||
/>
|
||||
</fieldset>
|
||||
) }
|
||||
{ ! DISPLAY_CART_PRICES_INCLUDING_TAX && (
|
||||
|
|
|
@ -10,9 +10,7 @@ import FormStep from '@woocommerce/base-components/checkout/form-step';
|
|||
import CheckoutForm from '@woocommerce/base-components/checkout/form';
|
||||
import NoShipping from '@woocommerce/base-components/checkout/no-shipping';
|
||||
import TextInput from '@woocommerce/base-components/text-input';
|
||||
import ShippingRatesControl, {
|
||||
Packages,
|
||||
} from '@woocommerce/base-components/shipping-rates-control';
|
||||
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
|
||||
import { CheckboxControl } from '@wordpress/components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
|
@ -23,6 +21,7 @@ import {
|
|||
} from '@woocommerce/base-components/payment-methods';
|
||||
import { SHIPPING_ENABLED } from '@woocommerce/block-settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useShippingRates } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -31,6 +30,7 @@ import './style.scss';
|
|||
import '../../../payment-methods-demo';
|
||||
|
||||
const Block = ( { attributes, isEditor = false, shippingRates = [] } ) => {
|
||||
const { shippingRatesLoading } = useShippingRates();
|
||||
const [ selectedShippingRate, setSelectedShippingRate ] = useState( {} );
|
||||
const [ contactFields, setContactFields ] = useState( {} );
|
||||
const [ shouldSavePayment, setShouldSavePayment ] = useState( true );
|
||||
|
@ -228,55 +228,33 @@ const Block = ( { attributes, isEditor = false, shippingRates = [] } ) => {
|
|||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
{ shippingRates.length > 0 ? (
|
||||
<Packages
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
selected={ [
|
||||
// This is only rendered in the editor, with placeholder
|
||||
// shippingRates, so we can safely fallback to set the
|
||||
// first shipping rate as selected and ignore setting
|
||||
// an onChange prop.
|
||||
shippingRates[ 0 ].shipping_rates[ 0 ]
|
||||
.rate_id,
|
||||
] }
|
||||
/>
|
||||
) : (
|
||||
<ShippingRatesControl
|
||||
address={
|
||||
shippingFields.country
|
||||
? {
|
||||
address_1:
|
||||
shippingFields.address_1,
|
||||
address_2:
|
||||
shippingFields.apartment,
|
||||
city: shippingFields.city,
|
||||
state: shippingFields.state,
|
||||
postcode:
|
||||
shippingFields.postcode,
|
||||
country:
|
||||
shippingFields.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
selected={ selectedShippingRate.methods }
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
onChange={ ( newMethods ) =>
|
||||
setSelectedShippingRate( {
|
||||
...selectedShippingRate,
|
||||
methods: newMethods,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<ShippingRatesControl
|
||||
address={
|
||||
shippingFields.country
|
||||
? {
|
||||
address_1:
|
||||
shippingFields.address_1,
|
||||
address_2:
|
||||
shippingFields.apartment,
|
||||
city: shippingFields.city,
|
||||
state: shippingFields.state,
|
||||
postcode:
|
||||
shippingFields.postcode,
|
||||
country: shippingFields.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={ shippingRatesLoading }
|
||||
/>
|
||||
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__add-note"
|
||||
label="Add order notes?"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { withRestApiHydration } from '@woocommerce/block-hocs';
|
||||
import { useStoreCart } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -10,6 +11,16 @@ import Block from './block.js';
|
|||
import blockAttributes from './attributes';
|
||||
import renderFrontend from '../../../utils/render-frontend.js';
|
||||
|
||||
/**
|
||||
* Wrapper component to supply API data.
|
||||
*
|
||||
* @param {Object} attributes object of key value attributes passed to block.
|
||||
*/
|
||||
const CheckoutFrontend = ( attributes ) => {
|
||||
const { shippingRates } = useStoreCart();
|
||||
return <Block attributes={ attributes } shippingRates={ shippingRates } />;
|
||||
};
|
||||
|
||||
const getProps = ( el ) => {
|
||||
const attributes = {};
|
||||
|
||||
|
@ -33,6 +44,6 @@ const getProps = ( el ) => {
|
|||
|
||||
renderFrontend(
|
||||
'.wp-block-woocommerce-checkout',
|
||||
withRestApiHydration( Block ),
|
||||
withRestApiHydration( CheckoutFrontend ),
|
||||
getProps
|
||||
);
|
||||
|
|
|
@ -7,4 +7,5 @@ export const ACTION_TYPES = {
|
|||
RECEIVE_CART_ITEM: 'RECEIVE_CART_ITEM',
|
||||
ITEM_QUANTITY_PENDING: 'ITEM_QUANTITY_PENDING',
|
||||
RECEIVE_REMOVED_ITEM: 'RECEIVE_REMOVED_ITEM',
|
||||
UPDATING_SHIPPING_ADDRESS: 'UPDATING_SHIPPING_ADDRESS',
|
||||
};
|
||||
|
|
|
@ -244,3 +244,66 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) {
|
|||
|
||||
yield itemQuantityPending( cartItemKey, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a shipping rate.
|
||||
*
|
||||
* @param {string} rateId the id of the rate being selected.
|
||||
* @param {number} [packageId] the key of the packages that we will select within.
|
||||
*/
|
||||
export function* selectShippingRate( rateId, packageId = 0 ) {
|
||||
try {
|
||||
const result = yield apiFetch( {
|
||||
path: `/wc/store/cart/select-shipping-rate/${ packageId }`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
rate_id: rateId,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
if ( result ) {
|
||||
yield receiveCart( result );
|
||||
}
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an action object used to track what shipping address are we updating to.
|
||||
*
|
||||
* @param {boolean} isResolving if we're loading shipping address or not.
|
||||
* @return {Object} Object for action.
|
||||
*/
|
||||
export function shippingRatesAreResolving( isResolving ) {
|
||||
return {
|
||||
type: types.UPDATING_SHIPPING_ADDRESS,
|
||||
isResolving,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a coupon code and either invalidates caches, or receives an error if
|
||||
the coupon cannot be applied.
|
||||
*
|
||||
* @param {Object} address shipping address to be updated
|
||||
*/
|
||||
export function* updateShippingAddress( address ) {
|
||||
yield shippingRatesAreResolving( true );
|
||||
try {
|
||||
const result = yield apiFetch( {
|
||||
path: '/wc/store/cart/update-shipping',
|
||||
method: 'POST',
|
||||
data: address,
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
if ( result ) {
|
||||
yield receiveCart( result );
|
||||
}
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
}
|
||||
yield shippingRatesAreResolving( false );
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -41,6 +46,7 @@ const reducer = (
|
|||
cartItemsQuantityPending: [],
|
||||
cartData: {
|
||||
coupons: [],
|
||||
shippingRates: [],
|
||||
items: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
|
@ -69,7 +75,9 @@ const reducer = (
|
|||
state = {
|
||||
...state,
|
||||
errors: [],
|
||||
cartData: action.response,
|
||||
cartData: mapKeys( action.response, ( _, key ) =>
|
||||
camelCase( key )
|
||||
),
|
||||
};
|
||||
break;
|
||||
case types.APPLYING_COUPON:
|
||||
|
@ -117,6 +125,15 @@ const reducer = (
|
|||
},
|
||||
};
|
||||
break;
|
||||
case types.UPDATING_SHIPPING_ADDRESS:
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
updatingShipping: action.isResolving,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { select, apiFetch } from '@wordpress/data-controls';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -25,11 +24,7 @@ export function* getCartData() {
|
|||
return;
|
||||
}
|
||||
|
||||
yield receiveCart(
|
||||
mapKeys( cartData, ( _value, key ) => {
|
||||
return camelCase( key );
|
||||
} )
|
||||
);
|
||||
yield receiveCart( cartData );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
/** @typedef { import('@woocommerce/type-defs/cart').CartData } CartData */
|
||||
/** @typedef { import('@woocommerce/type-defs/cart').CartTotals } CartTotals */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createRegistrySelector } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
|
||||
/**
|
||||
* Retrieves cart data from state.
|
||||
*
|
||||
|
@ -130,3 +140,18 @@ export const getCartItem = ( state, cartItemKey ) => {
|
|||
export const isItemQuantityPending = ( state, cartItemKey ) => {
|
||||
return state.cartItemsQuantityPending.includes( cartItemKey );
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves if the address is being applied for shipping.
|
||||
*
|
||||
* @param {Object} state The current state.
|
||||
* @return {boolean} are shipping rates loading.
|
||||
*/
|
||||
export const areShippingRatesLoading = createRegistrySelector(
|
||||
( select ) => ( state ) => {
|
||||
return !! (
|
||||
state.metaData.updatingShipping ||
|
||||
select( STORE_KEY ).isResolving( 'getCartData', [] )
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,12 +1,33 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
|
||||
export const previewShippingRates = [
|
||||
{
|
||||
destination: {},
|
||||
items: {},
|
||||
package_id: 0,
|
||||
name: __( 'Shipping', 'woo-gutenberg-products-block' ),
|
||||
items: [
|
||||
{
|
||||
key: '33e75ff09dd601bbe69f351039152189',
|
||||
name: _x(
|
||||
'Beanie with Logo',
|
||||
'example product in Cart Block',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
key: '6512bd43d9caa6e02c990b0a82652dca',
|
||||
name: _x(
|
||||
'Beanie',
|
||||
'example product in Cart Block',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_rates: [
|
||||
{
|
||||
currency_code: 'USD',
|
||||
|
@ -21,6 +42,8 @@ export const previewShippingRates = [
|
|||
delivery_time: '',
|
||||
price: '200',
|
||||
rate_id: 'free_shipping:1',
|
||||
method_id: 'flat_rate',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
currency_code: 'USD',
|
||||
|
@ -35,6 +58,8 @@ export const previewShippingRates = [
|
|||
delivery_time: '',
|
||||
price: '0',
|
||||
rate_id: 'local_pickup:1',
|
||||
method_id: 'local_pickup',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
* @typedef {Object} CartData
|
||||
*
|
||||
* @property {Array} coupons Coupons applied to cart.
|
||||
* @property {Array} shippingRates array of selected shipping rates
|
||||
* @property {Array} items Items in the cart.
|
||||
* @property {number} itemsCount Number of items in the cart.
|
||||
* @property {number} itemsWeight Weight of items in the cart.
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* @typedef {Object} StoreCart
|
||||
*
|
||||
* @property {Array} cartCoupons An array of coupons applied to the cart.
|
||||
* @property {Array} shippingRates array of selected shipping rates
|
||||
* @property {Array} cartItems An array of items in the cart.
|
||||
* @property {number} cartItemsCount The number of items in the cart.
|
||||
* @property {number} cartItemsWeight The weight of all items in the cart.
|
||||
|
@ -36,7 +37,7 @@
|
|||
* @property {Function} isPending Callback for determining if a cart
|
||||
* item is currently updating (i.e.
|
||||
* remove / change quantity).
|
||||
* @property {Function} changeQuantity Callback for changing quantity of item
|
||||
* @property {Function} changeQuantity Callback for changing quantity of item
|
||||
* in cart.
|
||||
* @property {Function} removeItem Callback for removing a cart item.
|
||||
*/
|
||||
|
|
|
@ -14,6 +14,7 @@ use \WP_Error as RestError;
|
|||
use \WP_REST_Server as RestServer;
|
||||
use \WP_REST_Controller as RestController;
|
||||
use \WC_REST_Exception as RestException;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartShippingRates;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\CartSchema;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
|
@ -141,8 +142,124 @@ class Cart extends RestController {
|
|||
'schema' => [ $this, 'get_public_item_schema' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/update-shipping',
|
||||
[
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [ $this, 'update_shipping' ],
|
||||
'args' => [
|
||||
'address_1' => array(
|
||||
'description' => __( 'First line of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
),
|
||||
'address_2' => [
|
||||
'description' => __( 'Second line of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'city' => [
|
||||
'description' => __( 'City of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'state' => [
|
||||
'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'postcode' => [
|
||||
'description' => __( 'Zip or Postcode of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'country' => [
|
||||
'description' => __( 'ISO code for the country of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this, 'get_public_item_schema' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/select-shipping-rate/(?P<package_id>[\d]+)',
|
||||
[
|
||||
'args' => [
|
||||
'package_id' => array(
|
||||
'description' => __( 'The ID of the package being shipped.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
),
|
||||
'rate_id' => [
|
||||
'description' => __( 'The chosen rate ID for the package.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [ $this, 'select_shipping_rate_for_package' ],
|
||||
],
|
||||
'schema' => [ $this, 'get_public_item_schema' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a shipping rate for the cart.
|
||||
*
|
||||
* This selects a shipping rate for a package and adds it to an array of selected shipping rates.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public function select_shipping_rate_for_package( $request ) {
|
||||
if ( ! wc_shipping_enabled() ) {
|
||||
return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $request['package_id'] ) || ! is_numeric( $request['package_id'] ) ) {
|
||||
return new RestError( 'woocommerce_rest_cart_missing_package_id', __( 'Invalid Package ID.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
$controller = new CartController();
|
||||
$cart = $controller->get_cart_instance();
|
||||
|
||||
if ( ! $cart || ! $cart instanceof \WC_Cart ) {
|
||||
return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( $cart->needs_shipping() ) {
|
||||
$package_id = absint( $request['package_id'] );
|
||||
$rate_id = wc_clean( wp_unslash( $request['rate_id'] ) );
|
||||
|
||||
try {
|
||||
$controller->select_shipping_rate( $package_id, $rate_id );
|
||||
} catch ( RestException $e ) {
|
||||
return new RestError( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
$cart->calculate_totals();
|
||||
|
||||
return rest_ensure_response( $this->prepare_item_for_response( $cart, $request ) );
|
||||
}
|
||||
/**
|
||||
* Apply a coupon to the cart.
|
||||
*
|
||||
|
@ -169,7 +286,6 @@ class Cart extends RestController {
|
|||
} catch ( RestException $e ) {
|
||||
return new RestError( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
|
||||
$data = $this->prepare_item_for_response( $cart, $request );
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
|
@ -211,6 +327,52 @@ class Cart extends RestController {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an address, gets updated shipping rates for the cart.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public function update_shipping( $request ) {
|
||||
if ( ! wc_shipping_enabled() ) {
|
||||
return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$controller = new CartController();
|
||||
$cart = $controller->get_cart_instance();
|
||||
|
||||
if ( ! $cart || ! $cart instanceof \WC_Cart ) {
|
||||
return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
$cart_shipping_rate_controller = new CartShippingRates();
|
||||
$request = $cart_shipping_rate_controller->validate_shipping_address( $request );
|
||||
|
||||
if ( is_wp_error( $request ) ) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
// Update customer session.
|
||||
WC()->customer->set_props(
|
||||
array(
|
||||
'shipping_country' => isset( $request['country'] ) ? $request['country'] : null,
|
||||
'shipping_state' => isset( $request['state'] ) ? $request['state'] : null,
|
||||
'shipping_postcode' => isset( $request['postcode'] ) ? $request['postcode'] : null,
|
||||
'shipping_city' => isset( $request['city'] ) ? $request['city'] : null,
|
||||
'shipping_address_1' => isset( $request['address_1'] ) ? $request['address_1'] : null,
|
||||
'shipping_address_2' => isset( $request['address_2'] ) ? $request['address_2'] : null,
|
||||
)
|
||||
);
|
||||
WC()->customer->save();
|
||||
|
||||
$cart->calculate_shipping();
|
||||
$cart->calculate_totals();
|
||||
|
||||
$data = $this->prepare_item_for_response( $cart, $request );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cart.
|
||||
*
|
||||
|
@ -296,8 +458,6 @@ class Cart extends RestController {
|
|||
* @return \WP_REST_Response Response object.
|
||||
*/
|
||||
public function prepare_item_for_response( $cart, $request ) {
|
||||
$data = $this->schema->get_item_response( $cart );
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
return rest_ensure_response( $this->schema->get_item_response( $cart ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,49 @@ class CartShippingRates extends RestController {
|
|||
'methods' => RestServer::READABLE,
|
||||
'callback' => [ $this, 'get_items' ],
|
||||
'args' => [
|
||||
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
|
||||
'address_1' => [
|
||||
'description' => __( 'First line of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'address_2' => [
|
||||
'description' => __( 'Second line of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'city' => [
|
||||
'description' => __( 'City of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'state' => [
|
||||
'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'postcode' => [
|
||||
'description' => __( 'Zip or Postcode of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'country' => [
|
||||
'description' => __( 'ISO code for the country of the address being shipped to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'wc_clean',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
|
||||
],
|
||||
],
|
||||
'schema' => [ $this, 'get_public_item_schema' ],
|
||||
|
@ -75,6 +117,10 @@ class CartShippingRates extends RestController {
|
|||
* @return \WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
if ( ! wc_shipping_enabled() ) {
|
||||
return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$controller = new CartController();
|
||||
$cart = $controller->get_cart_instance();
|
||||
|
||||
|
@ -82,24 +128,8 @@ class CartShippingRates extends RestController {
|
|||
return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $request['country'] ) ) {
|
||||
$valid_countries = WC()->countries->get_shipping_countries();
|
||||
|
||||
if (
|
||||
is_array( $valid_countries ) &&
|
||||
count( $valid_countries ) > 0 &&
|
||||
! array_key_exists( $request['country'], $valid_countries )
|
||||
) {
|
||||
return new RestError(
|
||||
'woocommerce_rest_cart_shipping_rates_invalid_country',
|
||||
sprintf(
|
||||
/* translators: 1: valid country codes */
|
||||
__( 'Destination country code is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ),
|
||||
implode( ', ', array_keys( $valid_countries ) )
|
||||
),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
if ( ! $cart->needs_shipping() ) {
|
||||
return rest_ensure_response( [] );
|
||||
}
|
||||
|
||||
$request = $this->validate_shipping_address( $request );
|
||||
|
@ -108,22 +138,21 @@ class CartShippingRates extends RestController {
|
|||
return $request;
|
||||
}
|
||||
|
||||
$cart_items = $controller->get_cart_items(
|
||||
function( $item ) {
|
||||
return ! empty( $item['data'] ) && $item['data']->needs_shipping();
|
||||
}
|
||||
$packages = $controller->get_shipping_packages(
|
||||
true,
|
||||
[
|
||||
'address_1' => $request['address_1'],
|
||||
'address_2' => $request['address_2'],
|
||||
'city' => $request['city'],
|
||||
'state' => $request['state'],
|
||||
'postcode' => $request['postcode'],
|
||||
'country' => $request['country'],
|
||||
]
|
||||
);
|
||||
|
||||
if ( empty( $cart_items ) ) {
|
||||
return rest_ensure_response( [] );
|
||||
}
|
||||
|
||||
$packages = $this->get_shipping_packages( $request );
|
||||
$response = [];
|
||||
|
||||
foreach ( $packages as $key => $package ) {
|
||||
$package['key'] = $key;
|
||||
$response[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $package, $request ) );
|
||||
foreach ( $packages as $package ) {
|
||||
$response[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $package, $request ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
|
@ -135,8 +164,39 @@ class CartShippingRates extends RestController {
|
|||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function validate_shipping_address( $request ) {
|
||||
$request['country'] = wc_strtoupper( $request['country'] );
|
||||
public function validate_shipping_address( $request ) {
|
||||
$valid_countries = WC()->countries->get_shipping_countries();
|
||||
|
||||
if ( empty( $request['country'] ) ) {
|
||||
return new RestError(
|
||||
'woocommerce_rest_cart_shipping_rates_missing_country',
|
||||
sprintf(
|
||||
/* translators: 1: valid country codes */
|
||||
__( 'No destination country code was given. Please provide one of the following: %s', 'woo-gutenberg-products-block' ),
|
||||
implode( ', ', array_keys( $valid_countries ) )
|
||||
),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$request['country'] = wc_strtoupper( $request['country'] );
|
||||
|
||||
if (
|
||||
is_array( $valid_countries ) &&
|
||||
count( $valid_countries ) > 0 &&
|
||||
! array_key_exists( $request['country'], $valid_countries )
|
||||
) {
|
||||
return new RestError(
|
||||
'woocommerce_rest_cart_shipping_rates_invalid_country',
|
||||
sprintf(
|
||||
/* translators: 1: valid country codes */
|
||||
__( 'Destination country code is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ),
|
||||
implode( ', ', array_keys( $valid_countries ) )
|
||||
),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$request['postcode'] = $request['postcode'] ? wc_format_postcode( $request['postcode'], $request['country'] ) : null;
|
||||
|
||||
if ( ! empty( $request['state'] ) ) {
|
||||
|
@ -249,34 +309,4 @@ class CartShippingRates extends RestController {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get packages with calculated shipping.
|
||||
*
|
||||
* Based on WC_Cart::get_shipping_packages but allows the destination to be
|
||||
* customised based on passed params.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return array of cart items
|
||||
*/
|
||||
protected function get_shipping_packages( $request ) {
|
||||
$packages = WC()->cart->get_shipping_packages();
|
||||
|
||||
if ( $request['country'] ) {
|
||||
foreach ( $packages as $key => $package ) {
|
||||
$packages[ $key ]['destination'] = [
|
||||
'address_1' => $request['address_1'],
|
||||
'address_2' => $request['address_2'],
|
||||
'city' => $request['city'],
|
||||
'state' => $request['state'],
|
||||
'postcode' => $request['postcode'],
|
||||
'country' => $request['country'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$packages = WC()->shipping()->calculate_shipping( $packages );
|
||||
|
||||
return $packages;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
|
@ -39,6 +41,16 @@ class CartSchema extends AbstractSchema {
|
|||
'properties' => $this->force_schema_readonly( ( new CartCouponSchema() )->get_properties() ),
|
||||
],
|
||||
],
|
||||
'shipping_rates' => [
|
||||
'description' => __( 'List of available shipping rates for the cart.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => $this->force_schema_readonly( ( new CartShippingRateSchema() )->get_properties() ),
|
||||
],
|
||||
],
|
||||
'items' => [
|
||||
'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
|
@ -171,12 +183,15 @@ class CartSchema extends AbstractSchema {
|
|||
* @return array
|
||||
*/
|
||||
public function get_item_response( $cart ) {
|
||||
$cart_coupon_schema = new CartCouponSchema();
|
||||
$cart_item_schema = new CartItemSchema();
|
||||
$context = 'edit';
|
||||
$controller = new CartController();
|
||||
$cart_coupon_schema = new CartCouponSchema();
|
||||
$cart_item_schema = new CartItemSchema();
|
||||
$shipping_rate_schema = new CartShippingRateSchema();
|
||||
$context = 'edit';
|
||||
|
||||
return [
|
||||
'coupons' => array_values( array_map( [ $cart_coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ),
|
||||
'shipping_rates' => array_values( array_map( [ $shipping_rate_schema, 'get_item_response' ], $controller->get_shipping_packages() ) ),
|
||||
'items' => array_values( array_map( [ $cart_item_schema, 'get_item_response' ], array_filter( $cart->get_cart() ) ) ),
|
||||
'items_count' => $cart->get_cart_contents_count(),
|
||||
'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ),
|
||||
|
|
|
@ -29,6 +29,18 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
*/
|
||||
protected function get_properties() {
|
||||
return [
|
||||
'package_id' => [
|
||||
'description' => __( 'The ID of the package the shipping rates belong to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name of the package.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'destination' => [
|
||||
'description' => __( 'Shipping destination address.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'object',
|
||||
|
@ -74,13 +86,19 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
],
|
||||
],
|
||||
'items' => [
|
||||
'description' => __( 'List of cart items (keys) the returned shipping rates apply to.', 'woo-gutenberg-products-block' ),
|
||||
'description' => __( 'List of cart items the returned shipping rates apply to.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'key' => [
|
||||
'description' => __( 'Unique identifier for the item within the cart.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name of the item.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
|
@ -96,12 +114,6 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
],
|
||||
],
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name of the package.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'shipping_rates' => [
|
||||
'description' => __( 'List of shipping rates.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
|
@ -122,8 +134,13 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
*/
|
||||
protected function get_rate_properties() {
|
||||
return array_merge(
|
||||
$this->get_store_currency_properties(),
|
||||
[
|
||||
'rate_id' => [
|
||||
'description' => __( 'ID of the shipping rate.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name of the shipping rate, e.g. Express shipping.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
|
@ -148,12 +165,6 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'rate_id' => [
|
||||
'description' => __( 'ID of the shipping rate.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'method_id' => [
|
||||
'description' => __( 'ID of the shipping method that provided the rate.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
|
@ -188,7 +199,14 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
'selected' => [
|
||||
'description' => __( 'True if this is the rate currently selected by the customer for the cart.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'boolean',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
],
|
||||
$this->get_store_currency_properties()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -202,13 +220,31 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
// Add product names and quantities.
|
||||
$items = array();
|
||||
foreach ( $package['contents'] as $item_id => $values ) {
|
||||
$items[ $item_id ] = [
|
||||
$items[] = [
|
||||
'key' => $item_id,
|
||||
'name' => $values['data']->get_name(),
|
||||
'quantity' => $values['quantity'],
|
||||
];
|
||||
}
|
||||
|
||||
// Generate package name.
|
||||
$package_number = absint( $package['package_id'] ) + 1;
|
||||
$package_display_name = apply_filters(
|
||||
'woocommerce_shipping_package_name',
|
||||
$package_number > 1 ?
|
||||
sprintf(
|
||||
/* translators: %d: shipping package number */
|
||||
_x( 'Shipping %d', 'shipping packages', 'woo-gutenberg-products-block' ),
|
||||
$package_number
|
||||
) :
|
||||
_x( 'Shipping', 'shipping packages', 'woo-gutenberg-products-block' ),
|
||||
$package['package_id'],
|
||||
$package
|
||||
);
|
||||
|
||||
return [
|
||||
'package_id' => $package['package_id'],
|
||||
'name' => $package_display_name,
|
||||
'destination' => (object) $this->prepare_html_response(
|
||||
[
|
||||
'address_1' => $package['destination']['address_1'],
|
||||
|
@ -220,38 +256,55 @@ class CartShippingRateSchema extends AbstractSchema {
|
|||
]
|
||||
),
|
||||
'items' => $items,
|
||||
'name' => apply_filters(
|
||||
'woocommerce_shipping_package_name',
|
||||
( $package['key'] > 0 ) ?
|
||||
/* translators: %d: shipping package number */
|
||||
sprintf( _x( 'Shipping %d', 'shipping packages', 'woo-gutenberg-products-block' ), ( $package['key'] + 1 ) ) :
|
||||
_x( 'Shipping', 'shipping packages', 'woo-gutenberg-products-block' ),
|
||||
$package['key'],
|
||||
$package
|
||||
),
|
||||
'shipping_rates' => array_values( array_map( [ $this, 'get_rate_response' ], $package['rates'] ) ),
|
||||
'shipping_rates' => $this->prepare_rates_response( $package ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of rates from a package for the response.
|
||||
*
|
||||
* @param array $package Shipping package complete with rates from WooCommerce.
|
||||
* @return array
|
||||
*/
|
||||
protected function prepare_rates_response( $package ) {
|
||||
$rates = $package['rates'];
|
||||
$selected_rates = WC()->session->get( 'chosen_shipping_methods', array() );
|
||||
$selected_rate = isset( $chosen_shipping_methods[ $package['package_id'] ] ) ? $chosen_shipping_methods[ $package['package_id'] ] : '';
|
||||
|
||||
if ( empty( $selected_rate ) && ! empty( $package['rates'] ) ) {
|
||||
$selected_rate = wc_get_chosen_shipping_method_for_package( $package['package_id'], $package );
|
||||
}
|
||||
|
||||
$response = [];
|
||||
|
||||
foreach ( $package['rates'] as $rate ) {
|
||||
$response[] = $this->get_rate_response( $rate, $selected_rate );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for a single rate.
|
||||
*
|
||||
* @param WC_Shipping_Rate $rate Rate object.
|
||||
* @param string $selected_rate Selected rate.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_rate_response( $rate ) {
|
||||
protected function get_rate_response( $rate, $selected_rate = '' ) {
|
||||
return array_merge(
|
||||
$this->get_store_currency_response(),
|
||||
[
|
||||
'rate_id' => $this->get_rate_prop( $rate, 'id' ),
|
||||
'name' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'label' ) ),
|
||||
'description' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'description' ) ),
|
||||
'delivery_time' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'delivery_time' ) ),
|
||||
'price' => $this->prepare_money_response( $this->get_rate_prop( $rate, 'cost' ), wc_get_price_decimals() ),
|
||||
'rate_id' => $this->get_rate_prop( $rate, 'id' ),
|
||||
'instance_id' => $this->get_rate_prop( $rate, 'instance_id' ),
|
||||
'method_id' => $this->get_rate_prop( $rate, 'method_id' ),
|
||||
'meta_data' => $this->get_rate_meta_data( $rate ),
|
||||
]
|
||||
'selected' => $selected_rate === $this->get_rate_prop( $rate, 'id' ),
|
||||
],
|
||||
$this->get_store_currency_response()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -209,6 +209,43 @@ class CartController {
|
|||
return $callback ? array_filter( wc()->cart->get_applied_coupons(), $callback ) : array_filter( wc()->cart->get_applied_coupons() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shipping packages from the cart and format as required.
|
||||
*
|
||||
* @param bool $calculate_rates Should rates for the packages also be returned.
|
||||
* @param array $destination Pass an address to override the package destination before calculation.
|
||||
* @return array
|
||||
*/
|
||||
public function get_shipping_packages( $calculate_rates = true, $destination = [] ) {
|
||||
$cart = $this->get_cart_instance();
|
||||
$packages = $cart->get_shipping_packages();
|
||||
|
||||
// Add package ID to array.
|
||||
foreach ( $packages as $key => $package ) {
|
||||
$packages[ $key ]['package_id'] = $key;
|
||||
|
||||
if ( ! empty( $destination ) ) {
|
||||
$packages[ $key ]['destination'] = $destination;
|
||||
}
|
||||
}
|
||||
|
||||
return $calculate_rates ? WC()->shipping()->calculate_shipping( $packages ) : $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a shipping rate.
|
||||
*
|
||||
* @param int $package_id ID of the package to choose a rate for.
|
||||
* @param string $rate_id ID of the rate being chosen.
|
||||
*/
|
||||
public function select_shipping_rate( $package_id, $rate_id ) {
|
||||
$cart = $this->get_cart_instance();
|
||||
$session_data = WC()->session->get( 'chosen_shipping_methods' ) ? WC()->session->get( 'chosen_shipping_methods' ) : [];
|
||||
$session_data[ $package_id ] = $rate_id;
|
||||
|
||||
WC()->session->set( 'chosen_shipping_methods', $session_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the core cart class but returns errors rather than rendering notices directly.
|
||||
*
|
||||
|
|
|
@ -11,6 +11,7 @@ use \WP_REST_Request;
|
|||
use \WC_REST_Unit_Test_Case as TestCase;
|
||||
use \WC_Helper_Product as ProductHelper;
|
||||
use \WC_Helper_Coupon as CouponHelper;
|
||||
use \WC_Helper_Shipping;
|
||||
use Automattic\WooCommerce\Blocks\Tests\Helpers\ValidateSchema;
|
||||
|
||||
/**
|
||||
|
@ -48,6 +49,8 @@ class Cart extends TestCase {
|
|||
$this->keys[] = wc()->cart->add_to_cart( $this->products[0]->get_id(), 2 );
|
||||
$this->keys[] = wc()->cart->add_to_cart( $this->products[1]->get_id(), 1 );
|
||||
wc()->cart->apply_coupon( $this->coupon->get_code() );
|
||||
|
||||
WC_Helper_Shipping::create_simple_flat_rate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,6 +59,10 @@ class Cart extends TestCase {
|
|||
public function test_register_routes() {
|
||||
$routes = $this->server->get_routes();
|
||||
$this->assertArrayHasKey( '/wc/store/cart', $routes );
|
||||
$this->assertArrayHasKey( '/wc/store/cart/apply-coupon', $routes );
|
||||
$this->assertArrayHasKey( '/wc/store/cart/remove-coupon', $routes );
|
||||
$this->assertArrayHasKey( '/wc/store/cart/update-shipping', $routes );
|
||||
$this->assertArrayHasKey( '/wc/store/cart/select-shipping-rate/(?P<package_id>[\d]+)', $routes );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,7 +75,7 @@ class Cart extends TestCase {
|
|||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 3, $data['items_count'] );
|
||||
$this->assertEquals( 2, count( $data['items'] ) );
|
||||
$this->assertEquals( false, $data['needs_shipping'] );
|
||||
$this->assertEquals( true, $data['needs_shipping'] );
|
||||
$this->assertEquals( '30', $data['items_weight'] );
|
||||
|
||||
$this->assertEquals( 'GBP', $data['totals']->currency_code );
|
||||
|
@ -151,6 +158,111 @@ class Cart extends TestCase {
|
|||
$this->assertEquals( '11000', $data['totals']->total_items );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting updated shipping.
|
||||
*/
|
||||
public function test_update_shipping() {
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'country' => 'US',
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertArrayHasKey( 'shipping_rates', $data );
|
||||
|
||||
$this->assertEquals( null, $data['shipping_rates'][0]['destination']->address_1 );
|
||||
$this->assertEquals( null, $data['shipping_rates'][0]['destination']->address_2 );
|
||||
$this->assertEquals( null, $data['shipping_rates'][0]['destination']->city );
|
||||
$this->assertEquals( null, $data['shipping_rates'][0]['destination']->state );
|
||||
$this->assertEquals( null, $data['shipping_rates'][0]['destination']->postcode );
|
||||
$this->assertEquals( 'US', $data['shipping_rates'][0]['destination']->country );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test shipping address validation.
|
||||
*/
|
||||
public function test_get_items_address_validation() {
|
||||
// US address.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'address_1' => 'Test address 1',
|
||||
'address_2' => 'Test address 2',
|
||||
'city' => 'Test City',
|
||||
'state' => 'AL',
|
||||
'postcode' => '90210',
|
||||
'country' => 'US'
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'Test address 1', $data['shipping_rates'][0]['destination']->address_1 );
|
||||
$this->assertEquals( 'Test address 2', $data['shipping_rates'][0]['destination']->address_2 );
|
||||
$this->assertEquals( 'Test City', $data['shipping_rates'][0]['destination']->city );
|
||||
$this->assertEquals( 'AL', $data['shipping_rates'][0]['destination']->state );
|
||||
$this->assertEquals( '90210', $data['shipping_rates'][0]['destination']->postcode );
|
||||
$this->assertEquals( 'US', $data['shipping_rates'][0]['destination']->country );
|
||||
|
||||
// Address with empty country.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'country' => ''
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 400, $response->get_status() );
|
||||
|
||||
// Address with invalid country.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'country' => 'ZZZZZZZZ'
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 400, $response->get_status() );
|
||||
|
||||
// US address with named state.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'state' =>'Alabama',
|
||||
'country' => 'US'
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'AL', $data['shipping_rates'][0]['destination']->state );
|
||||
$this->assertEquals( 'US', $data['shipping_rates'][0]['destination']->country );
|
||||
|
||||
// US address with invalid state.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'state' =>'ZZZZZZZZ',
|
||||
'country' => 'US'
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 400, $response->get_status() );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test applying coupon to cart.
|
||||
*/
|
||||
|
@ -230,6 +342,8 @@ class Cart extends TestCase {
|
|||
|
||||
$this->assertArrayHasKey( 'items_count', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'items', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'shipping_rates', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'coupons', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'needs_shipping', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'items_weight', $schema['properties'] );
|
||||
$this->assertArrayHasKey( 'totals', $schema['properties'] );
|
||||
|
@ -246,6 +360,8 @@ class Cart extends TestCase {
|
|||
|
||||
$this->assertArrayHasKey( 'items_count', $data );
|
||||
$this->assertArrayHasKey( 'items', $data );
|
||||
$this->assertArrayHasKey( 'shipping_rates', $data );
|
||||
$this->assertArrayHasKey( 'coupons', $data );
|
||||
$this->assertArrayHasKey( 'needs_shipping', $data );
|
||||
$this->assertArrayHasKey( 'items_weight', $data );
|
||||
$this->assertArrayHasKey( 'totals', $data );
|
||||
|
|
|
@ -103,7 +103,7 @@ class CartShippingRates extends TestCase {
|
|||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 400, $response->get_status() );
|
||||
|
||||
// Address with invalid country.
|
||||
$request = new WP_REST_Request( 'GET', '/wc/store/cart/shipping-rates' );
|
||||
|
@ -171,8 +171,8 @@ class CartShippingRates extends TestCase {
|
|||
*/
|
||||
public function test_prepare_item_for_response() {
|
||||
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartShippingRates();
|
||||
$packages = wc()->shipping->calculate_shipping( wc()->cart->get_shipping_packages() );
|
||||
$packages[0]['key'] = 0;
|
||||
$cart_controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController();
|
||||
$packages = $cart_controller->get_shipping_packages();
|
||||
$response = $controller->prepare_item_for_response( current( $packages ), [] );
|
||||
$data = $response->get_data();
|
||||
|
||||
|
|
Loading…
Reference in New Issue