Extensibility for Block Checkout Address Fields (https://github.com/woocommerce/woocommerce-blocks/pull/3662)

* Pass get_country_locale via assets

* Create helper to prepare and merge address fields

* Use new helper in address form

* Only pass field overrides now that fields are merged

* Fixc configs and remove lodash usage

* Prevent formatting of empty postcodes to suppress api error

* prevent memo on rerender

* Conitonal enqueue of locale data

* define index in increments of 10

* remove address-form

* circ deps changes

* Workaround for core data issue

* fix test

* remove prepareAddressFields export

* Remove old comment

* object from.entries polyfil

* Revert "object from.entries polyfil"

This reverts commit ba343adcf5fd2f843b225aebe340cce9b664c851.

* replace fromentries

* fix final fromentries
This commit is contained in:
Mike Jolley 2021-02-11 16:49:27 +00:00 committed by GitHub
parent 66fbd9738b
commit 7772d41007
11 changed files with 190 additions and 559 deletions

View File

@ -12,15 +12,16 @@ import {
ShippingStateInput,
} from '@woocommerce/base-components/state-input';
import { useValidationContext } from '@woocommerce/base-context';
import { useEffect } from '@wordpress/element';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@woocommerce/base-hocs/with-instance-id';
import { useShallowEqual } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import defaultAddressFields from './default-address-fields';
import countryAddressFields from './country-address-fields';
import prepareAddressFields from './prepare-address-fields';
// If it's the shipping address form and the user starts entering address
// values without having set the country first, show an error.
@ -76,18 +77,20 @@ const AddressForm = ( {
setValidationErrors,
clearValidationError,
} = useValidationContext();
const countryLocale = countryAddressFields[ values.country ] || {};
const addressFields = fields.map( ( field ) => ( {
key: field,
...defaultAddressFields[ field ],
...countryLocale[ field ],
...fieldConfig[ field ],
} ) );
const sortedAddressFields = addressFields.sort(
( a, b ) => a.index - b.index
);
const currentFields = useShallowEqual( fields );
const countryValidationError =
getValidationError( 'shipping-missing-country' ) || {};
const addressFormFields = useMemo( () => {
return prepareAddressFields(
currentFields,
fieldConfig,
values.country
);
}, [ currentFields, fieldConfig, values.country ] );
useEffect( () => {
if ( type === 'shipping' ) {
validateShippingCountry(
@ -111,7 +114,7 @@ const AddressForm = ( {
return (
<div id={ id } className="wc-block-components-address-form">
{ sortedAddressFields.map( ( field ) => {
{ addressFormFields.map( ( field ) => {
if ( field.hidden ) {
return null;
}

View File

@ -5,542 +5,75 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { getSetting } from '@woocommerce/settings';
/**
* Used to render postcode before the city field.
* This is locale data from WooCommerce countries class. This doesn't match the shape of the new field data blocks uses,
* but we can import part of it to set which fields are required.
*
* @type {Object <AddressFieldKey, AddressField>}
* This supports new properties such as optionalLabel which are not used by core (yet).
*/
const postcodeBeforeCity = {
city: {
index: 9,
},
postcode: {
index: 7,
},
};
const coreLocale = getSetting( 'countryLocale', {} );
/**
* Used to make the state field optional.
* Get supported props from the core locale and map to the correct format.
*
* @type {Object <AddressFieldKey, AddressField>}
* Ignores "class", "type", "placeholder", and "autocomplete"--blocks handles these visual elements.
*
* @param {Object} localeField Locale fields from WooCommerce.
* @return {Object} Supported locale fields.
*/
const optionalState = {
state: {
required: false,
},
const getSupportedProps = ( localeField ) => {
const fields = {};
if ( localeField.label !== undefined ) {
fields.label = localeField.label;
}
if ( localeField.required !== undefined ) {
fields.required = localeField.required;
}
if ( localeField.hidden !== undefined ) {
fields.hidden = localeField.hidden;
}
if ( localeField.label !== undefined && ! localeField.optionalLabel ) {
fields.optionalLabel = sprintf(
/* Translators: %s Field label. */
__( '%s (optional)', 'woo-gutenberg-products-block' ),
localeField.label
);
}
if ( localeField.priority ) {
fields.index = parseInt( localeField.priority, 10 );
}
if ( localeField.hidden === true ) {
fields.required = false;
}
return fields;
};
/**
* Used to hide the state field.
*
* @type {Object <AddressFieldKey, AddressField>}
*/
const hiddenState = {
state: {
required: false,
hidden: true,
},
};
const coreAddressFieldConfig = Object.entries( coreLocale )
.map( ( [ country, countryLocale ] ) => [
country,
Object.entries( countryLocale )
.map( ( [ localeFieldKey, localeField ] ) => [
localeFieldKey,
getSupportedProps( localeField ),
] )
.reduce( ( obj, [ key, val ] ) => {
obj[ key ] = val;
return obj;
}, {} ),
] )
.reduce( ( obj, [ key, val ] ) => {
obj[ key ] = val;
return obj;
}, {} );
/**
* Used to hide the postcode field.
*
* @type {Object <AddressFieldKey, AddressField>}
*/
const hiddenPostcode = {
postcode: {
required: false,
hidden: true,
},
};
/**
* Country specific address field properties.
*
* @type {CountryAddressFields}
*/
const countryAddressFields = {
AE: {
...hiddenPostcode,
...optionalState,
},
AF: hiddenState,
AO: {
...hiddenPostcode,
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
AT: {
...postcodeBeforeCity,
...hiddenState,
},
AU: {
city: {
label: __( 'Suburb', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Suburb (optional)',
'woo-gutenberg-products-block'
),
},
postcode: {
label: __( 'Postcode', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Postcode (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'State', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'State (optional)',
'woo-gutenberg-products-block'
),
},
},
AX: {
...postcodeBeforeCity,
...hiddenState,
},
BD: {
postcode: {
required: false,
},
state: {
label: __( 'District', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'District (optional)',
'woo-gutenberg-products-block'
),
},
},
BE: {
...postcodeBeforeCity,
...hiddenState,
},
BH: {
postcode: {
required: false,
},
...hiddenState,
},
BI: hiddenState,
BO: hiddenPostcode,
BS: hiddenPostcode,
CA: {
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
CH: {
...postcodeBeforeCity,
state: {
label: __( 'Canton', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Canton (optional)',
'woo-gutenberg-products-block'
),
required: false,
},
},
CL: {
city: {
require: true,
},
postcode: {
required: false,
},
state: {
label: __( 'Region', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Region (optional)',
'woo-gutenberg-products-block'
),
},
},
CN: {
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
CO: {
postcode: {
required: false,
},
},
CZ: hiddenState,
DE: {
...postcodeBeforeCity,
...hiddenState,
},
DK: {
...postcodeBeforeCity,
...hiddenState,
},
EE: {
...postcodeBeforeCity,
...hiddenState,
},
ES: {
...postcodeBeforeCity,
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
FI: {
...postcodeBeforeCity,
...hiddenState,
},
FR: {
...postcodeBeforeCity,
...hiddenState,
},
GB: {
postcode: {
label: __( 'Postcode', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Postcode (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'County', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'County (optional)',
'woo-gutenberg-products-block'
),
},
},
GP: hiddenState,
GF: hiddenState,
GR: optionalState,
HK: {
postcode: {
required: false,
},
city: {
label: __( 'Town/District', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Town/District (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'Region', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Region (optional)',
'woo-gutenberg-products-block'
),
},
},
HU: {
state: {
label: __( 'County', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'County (optional)',
'woo-gutenberg-products-block'
),
},
},
ID: {
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
IE: {
postcode: {
label: __( 'Eircode', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Eircode (optional)',
'woo-gutenberg-products-block'
),
required: false,
},
state: {
label: __( 'County', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'County (optional)',
'woo-gutenberg-products-block'
),
},
},
IS: {
...postcodeBeforeCity,
...hiddenState,
},
IL: {
...postcodeBeforeCity,
...hiddenState,
},
IM: hiddenState,
IT: {
...postcodeBeforeCity,
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
JP: {
first_name: {
index: 2,
},
last_name: {
index: 1,
},
address_1: {
index: 7,
},
address_2: {
index: 8,
},
postcode: {
index: 4,
},
city: {
index: 6,
},
state: {
label: __( 'Prefecture', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Prefecture (optional)',
'woo-gutenberg-products-block'
),
index: 5,
},
},
KR: hiddenState,
KW: hiddenState,
LB: hiddenState,
LI: {
...postcodeBeforeCity,
state: {
label: __( 'Municipality', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Municipality (optional)',
'woo-gutenberg-products-block'
),
required: false,
},
},
LK: hiddenState,
LU: hiddenState,
LV: {
state: {
label: __( 'Municipality', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Municipality (optional)',
'woo-gutenberg-products-block'
),
required: false,
},
},
MQ: hiddenState,
MT: hiddenState,
MZ: {
...hiddenPostcode,
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
NL: {
...postcodeBeforeCity,
...hiddenState,
},
NG: {
...hiddenPostcode,
state: {
label: __( 'State', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'State (optional)',
'woo-gutenberg-products-block'
),
},
},
NO: {
...postcodeBeforeCity,
...hiddenState,
},
NP: {
postcode: {
required: false,
},
state: {
label: __( 'State', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'State (optional)',
'woo-gutenberg-products-block'
),
},
},
NZ: {
postcode: {
label: __( 'Postcode', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Postcode (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'Region', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Region (optional)',
'woo-gutenberg-products-block'
),
},
},
PL: {
...postcodeBeforeCity,
...hiddenState,
},
PT: hiddenState,
RE: hiddenState,
RO: {
state: {
label: __( 'County', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'County (optional)',
'woo-gutenberg-products-block'
),
},
},
RS: hiddenState,
SE: {
...postcodeBeforeCity,
...hiddenState,
},
SG: {
city: {
required: false,
},
...hiddenState,
},
SK: {
...postcodeBeforeCity,
...hiddenState,
},
SI: {
...postcodeBeforeCity,
...hiddenState,
},
SR: {
...hiddenPostcode,
},
ST: {
...hiddenPostcode,
state: {
label: __( 'District', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'District (optional)',
'woo-gutenberg-products-block'
),
},
},
MD: {
state: {
label: __(
'Municipality/District',
'woo-gutenberg-products-block'
),
optionalLabel: __(
'Municipality/District (optional)',
'woo-gutenberg-products-block'
),
},
},
TR: {
...postcodeBeforeCity,
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
UG: {
...hiddenPostcode,
city: {
label: __( 'Town/Village', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Town/Village (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'District', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'District (optional)',
'woo-gutenberg-products-block'
),
},
},
US: {
postcode: {
label: __( 'ZIP', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'ZIP (optional)',
'woo-gutenberg-products-block'
),
},
state: {
label: __( 'State', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'State (optional)',
'woo-gutenberg-products-block'
),
},
},
VN: {
city: {
index: 8,
},
postcode: {
index: 7,
required: false,
},
...hiddenState,
},
WS: hiddenPostcode,
YT: hiddenState,
ZA: {
state: {
label: __( 'Province', 'woo-gutenberg-products-block' ),
optionalLabel: __(
'Province (optional)',
'woo-gutenberg-products-block'
),
},
},
ZW: hiddenPostcode,
};
export default countryAddressFields;
export default coreAddressFieldConfig;

View File

@ -29,7 +29,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: true,
hidden: false,
index: 1,
index: 10,
},
last_name: {
label: __( 'Last name', 'woo-gutenberg-products-block' ),
@ -41,7 +41,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: true,
hidden: false,
index: 2,
index: 20,
},
company: {
label: __( 'Company', 'woo-gutenberg-products-block' ),
@ -53,7 +53,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: false,
hidden: false,
index: 3,
index: 30,
},
address_1: {
label: __( 'Address', 'woo-gutenberg-products-block' ),
@ -65,7 +65,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: true,
hidden: false,
index: 4,
index: 40,
},
address_2: {
label: __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ),
@ -77,7 +77,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: false,
hidden: false,
index: 5,
index: 50,
},
country: {
label: __( 'Country/Region', 'woo-gutenberg-products-block' ),
@ -88,7 +88,7 @@ const AddressFields = {
autocomplete: 'country',
required: true,
hidden: false,
index: 6,
index: 60,
},
city: {
label: __( 'City', 'woo-gutenberg-products-block' ),
@ -97,7 +97,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: true,
hidden: false,
index: 7,
index: 70,
},
state: {
label: __( 'State/County', 'woo-gutenberg-products-block' ),
@ -109,7 +109,7 @@ const AddressFields = {
autocapitalize: 'sentences',
required: true,
hidden: false,
index: 8,
index: 80,
},
postcode: {
label: __( 'Postal code', 'woo-gutenberg-products-block' ),
@ -121,7 +121,7 @@ const AddressFields = {
autocapitalize: 'characters',
required: true,
hidden: false,
index: 9,
index: 90,
},
};

View File

@ -0,0 +1,39 @@
/** @typedef { import('@woocommerce/type-defs/address-fields').CountryAddressFields } CountryAddressFields */
/**
* Internal dependencies
*/
import defaultAddressFields from './default-address-fields';
import countryAddressFields from './country-address-fields';
/**
* Combines address fields, including fields from the locale, and sorts them by index.
*
* @param {Array} fields List of field keys--only address fields matching these will be returned.
* @param {Object} fieldConfigs Fields config contains field specific overrides at block level which may, for example, hide a field.
* @param {string} addressCountry Address country code. If unknown, locale fields will not be merged.
* @return {CountryAddressFields} Object containing address fields.
*/
const prepareAddressFields = ( fields, fieldConfigs, addressCountry = '' ) => {
const localeConfigs =
addressCountry && countryAddressFields[ addressCountry ] !== undefined
? countryAddressFields[ addressCountry ]
: {};
return fields
.map( ( field ) => {
const defaultConfig = defaultAddressFields[ field ] || {};
const localeConfig = localeConfigs[ field ] || {};
const fieldConfig = fieldConfigs[ field ] || {};
return {
key: field,
...defaultConfig,
...localeConfig,
...fieldConfig,
};
} )
.sort( ( a, b ) => a.index - b.index );
};
export default prepareAddressFields;

View File

@ -149,6 +149,9 @@ describe( 'AddressForm Component', () => {
// Verify state input has been removed.
expect( screen.queryByText( stateRegExp ) ).not.toBeInTheDocument();
inputAddress( tertiaryAddress );
// Verify postal code input label changed.
expect( screen.getByLabelText( /Postal code/ ) ).toBeInTheDocument();
} );

View File

@ -41,21 +41,14 @@ const AddressStep = ( {
const addressFieldsConfig = useMemo( () => {
return {
company: {
...defaultAddressFields.company,
hidden: ! showCompanyField,
required: requireCompanyField,
},
address_2: {
...defaultAddressFields.address_2,
hidden: ! showApartmentField,
},
};
}, [
defaultAddressFields,
showCompanyField,
requireCompanyField,
showApartmentField,
] );
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
return (
<>

View File

@ -9837,6 +9837,16 @@
"dev": true,
"optional": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
@ -15114,6 +15124,13 @@
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filelist": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@ -32534,6 +32551,7 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
}

View File

@ -79,6 +79,20 @@ class Cart extends AbstractBlock {
$data_registry->add( 'shippingStates', $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ) );
}
if ( ! $data_registry->exists( 'countryLocale' ) ) {
// Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
$country_locale = wc()->countries->get_country_locale();
$states = wc()->countries->get_states();
foreach ( $states as $country => $states ) {
if ( empty( $states ) ) {
$country_locale[ $country ]['state']['required'] = false;
$country_locale[ $country ]['state']['hidden'] = true;
}
}
$data_registry->add( 'countryLocale', $country_locale );
}
$permalink = ! empty( $attributes['checkoutPageId'] ) ? get_permalink( $attributes['checkoutPageId'] ) : false;
if ( $permalink && ! $data_registry->exists( 'page-' . $attributes['checkoutPageId'] ) ) {

View File

@ -101,6 +101,20 @@ class Checkout extends AbstractBlock {
$data_registry->add( 'shippingStates', $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ) );
}
if ( ! $data_registry->exists( 'countryLocale' ) ) {
// Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
$country_locale = wc()->countries->get_country_locale();
$states = wc()->countries->get_states();
foreach ( $states as $country => $states ) {
if ( empty( $states ) ) {
$country_locale[ $country ]['state']['required'] = false;
$country_locale[ $country ]['state']['hidden'] = true;
}
}
$data_registry->add( 'countryLocale', $country_locale );
}
$permalink = ! empty( $attributes['cartPageId'] ) ? get_permalink( $attributes['cartPageId'] ) : false;
if ( $permalink && ! $data_registry->exists( 'page-' . $attributes['cartPageId'] ) ) {

View File

@ -95,7 +95,7 @@ abstract class AbstractAddressSchema extends AbstractSchema {
$address['address_2'] = wc_clean( wp_unslash( $address['address_2'] ) );
$address['city'] = wc_clean( wp_unslash( $address['city'] ) );
$address['state'] = $this->format_state( wc_clean( wp_unslash( $address['state'] ) ), $address['country'] );
$address['postcode'] = wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] );
$address['postcode'] = $address['postcode'] ? wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
return $address;
}

View File

@ -38,6 +38,20 @@ global.wcSettings = {
ON: 'Ontario',
},
},
countryLocale: {
GB: {
postcode: { label: 'Postcode' },
state: { label: 'County', required: false },
},
AT: {
postcode: { priority: 65 },
state: { required: false, hidden: true },
},
CA: {
postcode: { label: 'Postal code' },
state: { label: 'Province' },
},
},
};
global.jQuery = () => ( {