* Include shipping and billing address data in cart schema

* update cart hook (and data api) with new properties from endpoint

* add use-shipping-address hook and implement in use-shipping-rates

* update usages of useShippingRates through code

* update tests for use-shipping-rates

* update use-payment-method-interface and typedef to remove country field

This is provided reliably via the shippingAddress now.

* restore pluck comparison to effect.

Also added some clarification docs for why `iniitalAddress` is not included in the effect dependencies.

* remove billingAddress from cart schema

* clear city and postcode when changing country

* Update REST Api schemas aftere rebase

- CustomerSchema no longer exists
- Added ShippingAddressSchema implemented by Cart and Order schemas
- fix broken js because of bad merge conflict resolution.

* remove duplicate keys
This commit is contained in:
Darren Ethier 2020-03-13 15:04:03 -04:00 committed by GitHub
parent 5d1d8f0394
commit 80f692404f
16 changed files with 267 additions and 149 deletions

View File

@ -66,6 +66,8 @@ const AddressForm = ( {
...values,
country: newValue,
state: '',
city: '',
postcode: '',
} )
}
required={ field.required }

View File

@ -14,7 +14,6 @@ import { previewCart } from '@woocommerce/resource-previews';
*/
const defaultCartData = {
cartCoupons: [],
shippingRates: [],
cartItems: [],
cartItemsCount: 0,
cartItemsWeight: 0,
@ -22,6 +21,18 @@ const defaultCartData = {
cartTotals: {},
cartIsLoading: true,
cartErrors: [],
shippingAddress: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
shippingRates: [],
};
/**
@ -43,7 +54,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
const results = useSelect(
( select ) => {
if ( ! shouldSelect ) {
return null;
return defaultCartData;
}
if ( isEditor ) {
@ -70,6 +81,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
return {
cartCoupons: cartData.coupons,
shippingRates: cartData.shippingRates,
shippingAddress: cartData.shippingAddress,
cartItems: cartData.items,
cartItemsCount: cartData.itemsCount,
cartItemsWeight: cartData.itemsWeight,
@ -81,8 +93,5 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
},
[ shouldSelect ]
);
if ( results === null ) {
return defaultCartData;
}
return results;
};

View File

@ -169,9 +169,6 @@ export const usePaymentMethodInterface = () => {
orderLoading,
cartTotal: currentCartTotal.current,
currency: getCurrencyFromPriceResponse( cartTotals ),
// @todo need to pass along the default country set for the site
// if it's available.
country: '',
cartTotalItems: currentCartTotals.current,
displayPricesIncludingTax: DISPLAY_CART_PRICES_INCLUDING_TAX,
appliedCoupons,

View File

@ -1,2 +1,3 @@
export * from './use-select-shipping-rate';
export * from './use-shipping-rates';
export * from './use-shipping-address';

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { useDispatch } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { useDebounce } from 'use-debounce';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import { useStoreCart } from '../cart/use-store-cart';
import { pluckAddress } from '../../utils';
const shouldUpdateStore = ( oldAddress, newAddress ) =>
! isShallowEqual( pluckAddress( oldAddress ), pluckAddress( newAddress ) );
export const useShippingAddress = () => {
const { shippingAddress: initialAddress } = useStoreCart();
const [ shippingAddress, setShippingAddress ] = useState( initialAddress );
const [ debouncedShippingAddress ] = useDebounce( shippingAddress, 400 );
const { updateShippingAddress } = useDispatch( storeKey );
// Note, we're intentionally not using initialAddress as a dependency here
// so that the stale (previous) value is being used for comparison.
useEffect( () => {
if (
debouncedShippingAddress.country &&
shouldUpdateStore( initialAddress, debouncedShippingAddress )
) {
updateShippingAddress( debouncedShippingAddress );
}
}, [ debouncedShippingAddress ] );
return {
shippingAddress,
setShippingAddress,
};
};

View File

@ -1,24 +1,19 @@
/**
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useReducer, useEffect } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { useDebounce } from 'use-debounce';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import { useStoreCart } from '../cart/use-store-cart';
import { pluckAddress } from '../../utils';
import { useShippingAddress } from '../shipping/use-shipping-address';
/**
* This is a custom hook that is wired up to the `wc/store/cart/shipping-rates` route.
* Given a a set of default fields keys, this will handle shipping form state and load
* new rates when certain fields change.
*
* @param {Array} addressFieldsKeys an array containing default fields keys.
*
* @return {Object} This hook will return an object with three properties:
* - {Boolean} shippingRatesLoading A boolean indicating whether the shipping
* rates are still loading or not.
@ -27,39 +22,13 @@ import { pluckAddress } from '../../utils';
* - {Object} shippingAddress An object containing shipping address.
* - {Object} shippingAddress True when address data exists.
*/
export const useShippingRates = ( addressFieldsKeys ) => {
export const useShippingRates = () => {
const { cartErrors, shippingRates } = useStoreCart();
const initialAddress = shippingRates[ 0 ]?.destination || {};
addressFieldsKeys.forEach( ( key ) => {
initialAddress[ key ] = initialAddress[ key ] || '';
} );
const shippingAddressReducer = ( state, address ) => ( {
...state,
...address,
} );
const [ shippingAddress, setShippingAddress ] = useReducer(
shippingAddressReducer,
initialAddress
);
const [ debouncedShippingAddress ] = useDebounce( shippingAddress, 400 );
const { shippingAddress, setShippingAddress } = useShippingAddress();
const shippingRatesLoading = useSelect(
( select ) => select( storeKey ).areShippingRatesLoading(),
[]
);
const { updateShippingAddress } = useDispatch( storeKey );
useEffect( () => {
if (
! isShallowEqual(
pluckAddress( debouncedShippingAddress ),
pluckAddress( initialAddress )
) &&
debouncedShippingAddress.country
) {
updateShippingAddress( debouncedShippingAddress );
}
}, [ debouncedShippingAddress ] );
return {
shippingRates,
shippingAddress,

View File

@ -38,9 +38,8 @@ describe( 'useShippingRates', () => {
</RegistryProvider>
);
const defaultFieldsConfig = [ 'country', 'state', 'city', 'postcode' ];
const getTestComponent = () => () => {
const items = useShippingRates( defaultFieldsConfig );
const items = useShippingRates();
return <div { ...items } />;
};
@ -50,6 +49,12 @@ describe( 'useShippingRates', () => {
itemsCount: 123,
itemsWeight: 123,
needsShipping: false,
shippingAddress: {
country: '',
state: '',
city: '',
postcode: '',
},
shippingRates: [
{
shippingRates: [ { foo: 'bar' } ],
@ -94,9 +99,7 @@ describe( 'useShippingRates', () => {
} );
const { shippingAddress } = getProps( renderer );
expect( shippingAddress ).toStrictEqual(
mockCartData.shippingRates[ 0 ].destination
);
expect( shippingAddress ).toStrictEqual( mockCartData.shippingAddress );
// rerender
act( () => {
renderer.update( getWrappedComponents( TestComponent ) );

View File

@ -89,7 +89,7 @@ const Block = ( {
shippingRatesLoading,
shippingAddress: shippingFields,
setShippingAddress: setShippingFields,
} = useShippingRates( Object.keys( addressFields ) );
} = useShippingRates();
return (
<CheckoutProvider isEditor={ isEditor }>

View File

@ -47,6 +47,17 @@ const reducer = (
cartData: {
coupons: [],
shippingRates: [],
shippingAddress: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
items: [],
itemsCount: 0,
itemsWeight: 0,

View File

@ -179,13 +179,17 @@
/**
* @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.
* @property {boolean} needsShipping True if the cart needs shipping.
* @property {CartTotals} totals Cart total amounts.
* @property {Array} coupons Coupons applied to cart.
* @property {Array} shippingRates Array of selected shipping
* rates.
* @property {CartShippingAddress} shippingAddress Shipping address for the
* cart.
* @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.
* @property {boolean} needsShipping True if the cart needs
* shipping.
* @property {CartTotals} totals Cart total amounts.
*/
/**

View File

@ -1,19 +1,32 @@
/**
* @typedef {import('./cart').CartData} CartData
* @typedef {import('./cart').CartBillingAddress} CartBillingAddress
* @typedef {import('./cart').CartShippingAddress} CartShippingAddress
*/
/**
* @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.
* @property {boolean} cartNeedsShipping True when the cart will require shipping.
* @property {Object} cartTotals Cart and line total amounts.
* @property {boolean} cartIsLoading True when cart data is being loaded.
* @property {Array} cartErrors An array of errors thrown by the cart.
* @property {Array} cartCoupons An array of coupons applied
* to the cart.
* @property {Array} shippingRates array of selected shipping
* rates
* @property {CartShippingAddress} shippingAddress Shipping address for the
* cart.
* @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.
* @property {boolean} cartNeedsShipping True when the cart will
* require shipping.
* @property {Object} cartTotals Cart and line total
* amounts.
* @property {boolean} cartIsLoading True when cart data is
* being loaded.
* @property {Array} cartErrors An array of errors thrown
* by the cart.
*/
/**

View File

@ -130,10 +130,6 @@
* @property {CartTotalItem} cartTotal The total item for
* the cart.
* @property {SiteCurrency} currency Currency object.
* @property {string} country ISO country code
* for the default
* country for the
* site.
* @property {CartTotalItem[]} cartTotalItems The various subtotal
* amounts.
* @property {boolean} displayPricesIncludingTax True means that the

View File

@ -28612,7 +28612,7 @@
}
},
"woocommerce": {
"version": "git+https://github.com/woocommerce/woocommerce.git#fb1001b14a90b3020ca8000e424c955845142f5c",
"version": "git+https://github.com/woocommerce/woocommerce.git#6f2b232fc7fa53417691a742a419147bb0134a5c",
"from": "git+https://github.com/woocommerce/woocommerce.git",
"dev": true,
"requires": {

View File

@ -31,7 +31,7 @@ class CartSchema extends AbstractSchema {
*/
public function get_properties() {
return [
'coupons' => [
'coupons' => [
'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
@ -41,7 +41,7 @@ class CartSchema extends AbstractSchema {
'properties' => $this->force_schema_readonly( ( new CartCouponSchema() )->get_properties() ),
],
],
'shipping_rates' => [
'shipping_rates' => [
'description' => __( 'List of available shipping rates for the cart.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
@ -51,7 +51,17 @@ class CartSchema extends AbstractSchema {
'properties' => $this->force_schema_readonly( ( new CartShippingRateSchema() )->get_properties() ),
],
],
'items' => [
'shipping_address' => [
'description' => __( 'Current set shipping address for the customer.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( ( new ShippingAddressSchema() )->get_properties() ),
],
],
'items' => [
'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
@ -61,25 +71,25 @@ class CartSchema extends AbstractSchema {
'properties' => $this->force_schema_readonly( ( new CartItemSchema() )->get_properties() ),
],
],
'items_count' => [
'items_count' => [
'description' => __( 'Number of items in the cart.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'items_weight' => [
'items_weight' => [
'description' => __( 'Total weight (in grams) of all products in the cart.', 'woo-gutenberg-products-block' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_shipping' => [
'needs_shipping' => [
'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'totals' => [
'description' => __( 'Cart total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
@ -183,20 +193,22 @@ class CartSchema extends AbstractSchema {
* @return array
*/
public function get_item_response( $cart ) {
$controller = new CartController();
$cart_coupon_schema = new CartCouponSchema();
$cart_item_schema = new CartItemSchema();
$shipping_rate_schema = new CartShippingRateSchema();
$context = 'edit';
$controller = new CartController();
$cart_coupon_schema = new CartCouponSchema();
$cart_item_schema = new CartItemSchema();
$shipping_rate_schema = new CartShippingRateSchema();
$shipping_address_schema = ( new ShippingAddressSchema() )->get_item_response( WC()->customer );
$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' ),
'needs_shipping' => $cart->needs_shipping(),
'totals' => (object) array_merge(
'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() ) ),
'shipping_address' => $shipping_address_schema,
'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' ),
'needs_shipping' => $cart->needs_shipping(),
'totals' => (object) array_merge(
$this->get_store_currency_response(),
[
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),

View File

@ -219,53 +219,7 @@ class OrderSchema extends AbstractSchema {
'description' => __( 'Shipping address.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'properties' => [
'first_name' => [
'description' => __( 'First name', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'last_name' => [
'description' => __( 'Last name', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'company' => [
'description' => __( 'Company', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_1' => [
'description' => __( 'Address', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_2' => [
'description' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'city' => [
'description' => __( 'City', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'state' => [
'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'postcode' => [
'description' => __( 'Postal code', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'country' => [
'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
],
'properties' => $this->force_schema_readonly( ( new ShippingAddressSchema() )->get_properties() ),
],
'coupons' => [
'description' => __( 'List of applied coupons.', 'woo-gutenberg-products-block' ),
@ -422,19 +376,7 @@ class OrderSchema extends AbstractSchema {
'phone' => $order->get_billing_phone(),
]
),
'shipping_address' => (object) $this->prepare_html_response(
[
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
]
),
'shipping_address' => ( new ShippingAddressSchema() )->get_item_response( $order ),
'coupons' => array_values( array_map( [ $order_coupon_schema, 'get_item_response' ], $order->get_items( 'coupon' ) ) ),
'items' => array_values( array_map( [ $order_item_schema, 'get_item_response' ], $order->get_items( 'line_item' ) ) ),
'totals' => (object) array_merge(

View File

@ -0,0 +1,120 @@
<?php
/**
* Shipping Address Schema.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\Routes;
/**
* ShippingAddressSchema class.
*
* Provides a generic shipping address schema for composition in other schemas.
*
* @since 2.5.0
*/
class ShippingAddressSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'shipping_address';
/**
* Term properties.
*
* @return array
*/
public function get_properties() {
return [
'first_name' => [
'description' => __( 'First name', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'last_name' => [
'description' => __( 'Last name', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'company' => [
'description' => __( 'Company', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_1' => [
'description' => __( 'Address', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_2' => [
'description' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'city' => [
'description' => __( 'City', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'state' => [
'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'postcode' => [
'description' => __( 'Postal code', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'country' => [
'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
];
}
/**
* Convert a term object into an object suitable for the response.
*
* @param \WC_Order|\WC_Customer $address An object with shipping address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
*/
public function get_item_response( $address ) {
if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) {
return (object) $this->prepare_html_response(
[
'first_name' => $address->get_shipping_first_name(),
'last_name' => $address->get_shipping_last_name(),
'company' => $address->get_shipping_company(),
'address_1' => $address->get_shipping_address_1(),
'address_2' => $address->get_shipping_address_2(),
'city' => $address->get_shipping_city(),
'state' => $address->get_shipping_state(),
'postcode' => $address->get_shipping_postcode(),
'country' => $address->get_shipping_country(),
]
);
}
throw new RouteException(
'invalid_object_type',
sprintf(
/* translators: Placeholders are class and method names */
__( '%1$s requires an instance of %2$s or %3$s for the address', 'woo-gutenberg-products-block' ),
'ShippingAddress::get_item_response',
'WC_Customer',
'WC_Order'
),
500
);
}
}