Use address format from server in address card (#45852)

This commit is contained in:
Thomas Roberts 2024-04-08 09:43:42 +01:00 committed by GitHub
parent 79d65eeea0
commit 9f159f7141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 535 additions and 39 deletions

View File

@ -2,15 +2,15 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ALLOWED_COUNTRIES, ALLOWED_STATES } from '@woocommerce/block-settings';
import {
type CartShippingAddress,
type CartBillingAddress,
isObject,
type CountryData,
objectHasProp,
isString,
} from '@woocommerce/types';
import { FormFieldsConfig } from '@woocommerce/settings';
import { decodeEntities } from '@wordpress/html-entities';
import { FormFieldsConfig, getSetting } from '@woocommerce/settings';
import { formatAddress } from '@woocommerce/blocks/checkout/utils';
/**
* Internal dependencies
@ -28,33 +28,37 @@ const AddressCard = ( {
target: string;
fieldConfig: FormFieldsConfig;
} ): JSX.Element | null => {
const formattedCountry = isString( ALLOWED_COUNTRIES[ address.country ] )
? decodeEntities( ALLOWED_COUNTRIES[ address.country ] )
: '';
const countryData = getSetting< Record< string, CountryData > >(
'countryData',
{}
);
const formattedState =
isObject( ALLOWED_STATES[ address.country ] ) &&
isString( ALLOWED_STATES[ address.country ][ address.state ] )
? decodeEntities(
ALLOWED_STATES[ address.country ][ address.state ]
)
: address.state;
let formatToUse = getSetting< string >(
'defaultAddressFormat',
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}'
);
if (
objectHasProp( countryData, address?.country ) &&
objectHasProp( countryData[ address.country ], 'format' ) &&
isString( countryData[ address.country ].format )
) {
// `as string` is fine here because we check if it's a string above.
formatToUse = countryData[ address.country ].format as string;
}
const { name: formattedName, address: formattedAddress } = formatAddress(
address,
formatToUse
);
return (
<div className="wc-block-components-address-card">
<address>
<span className="wc-block-components-address-card__address-section">
{ address.first_name + ' ' + address.last_name }
{ formattedName }
</span>
<div className="wc-block-components-address-card__address-section">
{ [
address.address_1,
! fieldConfig.address_2.hidden && address.address_2,
address.city,
formattedState,
address.postcode,
formattedCountry,
]
{ formattedAddress
.filter( ( field ) => !! field )
.map( ( field, index ) => (
<span key={ `address-` + index }>{ field }</span>

View File

@ -148,7 +148,8 @@ describe( 'Testing cart', () => {
return Promise.resolve( '' );
} );
} );
render( <CheckoutBlock /> );
const { rerender } = render( <CheckoutBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
expect(
@ -156,7 +157,63 @@ describe( 'Testing cart', () => {
).toBeInTheDocument();
expect(
screen.getByText( 'Ontario', {
screen.getByText( 'Toronto ON M4W 1A6', {
selector: '.wc-block-components-address-card span',
} )
).toBeInTheDocument();
// Async is needed here despite the IDE warnings. Testing Library gives a warning if not awaited.
await act( () =>
dispatch( CART_STORE_KEY ).setShippingAddress( {
first_name: 'First Name JP',
last_name: 'Last Name JP',
company: '',
address_1: 'Address 1 JP',
address_2: '',
city: 'Kobe',
state: 'JP28',
postcode: '650-0000',
country: 'JP',
phone: '',
} )
);
rerender( <CheckoutBlock /> );
expect(
screen.getByText( 'Hyogo Kobe Address 1 JP', {
selector: '.wc-block-components-address-card span',
} )
).toBeInTheDocument();
// Testing the default address format
await act( () =>
dispatch( CART_STORE_KEY ).setShippingAddress( {
first_name: 'First Name GB',
last_name: 'Last Name GB',
company: '',
address_1: 'Address 1 GB',
address_2: '',
city: 'Liverpool',
state: 'Merseyside',
postcode: 'L1 0BP',
country: 'GB',
phone: '',
} )
);
rerender( <CheckoutBlock /> );
expect(
screen.getByText( 'Liverpool', {
selector: '.wc-block-components-address-card span',
} )
).toBeInTheDocument();
expect(
screen.getByText( 'Merseyside', {
selector: '.wc-block-components-address-card span',
} )
).toBeInTheDocument();
expect(
screen.getByText( 'L1 0BP', {
selector: '.wc-block-components-address-card span',
} )
).toBeInTheDocument();

View File

@ -0,0 +1,269 @@
/**
* External dependencies
*/
import { CartBillingAddress } from '@woocommerce/type-defs/cart';
import { extractName, formatAddress } from '@woocommerce/blocks/checkout/utils';
describe( 'extractName', () => {
it.each( [
[
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
'{name}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}{name}',
'{name}',
],
[
'{first_name} {last_name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
'{first_name} {last_name}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{first_name} {last_name}\n{postcode}\n{country}',
'{first_name} {last_name}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{first_name_upper} {last_name}\n{postcode}\n{country}',
'{first_name_upper} {last_name}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{first_name_upper} {last_name_upper}\n{postcode}\n{country}',
'{first_name_upper} {last_name_upper}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{first_name} {last_name_upper}\n{postcode}\n{country}',
'{first_name} {last_name_upper}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{last_name_upper} {first_name} \n{postcode}\n{country}',
'{last_name_upper} {first_name}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{name_upper}\n{postcode}\n{country}',
'{name_upper}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{last_name_upper} {first_name_upper} \n{postcode}\n{country}',
'{last_name_upper} {first_name_upper}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{last_name} {first_name_upper} \n{postcode}\n{country}',
'{last_name} {first_name_upper}',
],
[
'{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{last_name} {first_name} \n{postcode}\n{country}',
'{last_name} {first_name}',
],
] )(
'should extract the name token from the format',
( format, expected ) => {
expect( extractName( format ) ).toBe( expected );
}
);
} );
describe( 'formatAddress', () => {
const defaultAddress: CartBillingAddress = {
first_name: 'John',
last_name: 'Doe',
address_1: '123 Yonge St',
address_2: 'Apt 1',
city: 'Toronto',
state: 'ON',
postcode: 'M5B1M4',
country: 'CA',
email: 'jon.doe@mail.com',
company: 'WooCommerce',
phone: '1234567890',
};
it.each( [
[
defaultAddress,
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'John Doe',
address: [
'WooCommerce',
'123 Yonge St',
'Apt 1',
'Toronto',
'Ontario',
'M5B1M4',
'Canada',
],
},
],
// Switch name so it's not the first thing.
[
defaultAddress,
'{company}\n{name}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'John Doe',
address: [
'WooCommerce',
'123 Yonge St',
'Apt 1',
'Toronto',
'Ontario',
'M5B1M4',
'Canada',
],
},
],
// Try with upper case name.
[
defaultAddress,
'{company}\n{name_upper}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'JOHN DOE',
address: [
'WooCommerce',
'123 Yonge St',
'Apt 1',
'Toronto',
'Ontario',
'M5B1M4',
'Canada',
],
},
],
// Try with upper case first name and regular last name.
[
defaultAddress,
'{company}\n{last_name} {first_name_upper}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'Doe JOHN',
address: [
'WooCommerce',
'123 Yonge St',
'Apt 1',
'Toronto',
'Ontario',
'M5B1M4',
'Canada',
],
},
],
// Try with regular first name and upper case last name.
[
defaultAddress,
'{company}\n{last_name} {first_name_upper}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'Doe JOHN',
address: [
'WooCommerce',
'123 Yonge St',
'Apt 1',
'Toronto',
'Ontario',
'M5B1M4',
'Canada',
],
},
],
// Try with upper case values.
[
defaultAddress,
'{company_upper}\n{name}\n{address_1_upper}\n{address_2_upper}\n{city_upper}\n{state_upper}\n{postcode_upper}\n{country_upper}',
{
name: 'John Doe',
address: [
'WOOCOMMERCE',
'123 YONGE ST',
'APT 1',
'TORONTO',
'ONTARIO',
'M5B1M4',
'CANADA',
],
},
],
// Try with missing values.
[
defaultAddress,
'{company_upper}\n{name}\n\n\n{address_2_upper}\n{city_upper}\n{postcode_upper}\n{country_upper}',
{
name: 'John Doe',
address: [
'WOOCOMMERCE',
'APT 1',
'TORONTO',
'M5B1M4',
'CANADA',
],
},
],
// Try with an empty string.
[
defaultAddress,
'',
{
name: '',
address: [],
},
],
// Try with a badly mangled string.
[
defaultAddress,
'{company_uppe}\n{name}\n\n\naddress_2_upper!\n{city_upper}£postcode_upper}\n{country_upper',
{
name: 'John Doe',
address: [
'{company_uppe}',
'address_2_upper!',
'TORONTO£postcode_upper}',
'{country_upper',
],
},
],
// Test empty address values.
[
{
first_name: '',
last_name: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
email: 'jon.doe@mail.com',
company: 'WooCommerce',
phone: '1234567890',
},
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: '',
address: [ 'WooCommerce' ],
},
],
// Test partial address values.
[
{
first_name: 'Jon',
last_name: '',
address_1: '',
address_2: '',
city: 'Toronto',
state: '',
postcode: '',
country: '',
email: 'jon.doe@mail.com',
company: 'WooCommerce',
phone: '1234567890',
},
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
{
name: 'Jon',
address: [ 'WooCommerce', 'Toronto' ],
},
],
] )(
'should format the address correctly',
( address, format, expected ) => {
const formattedAddress = formatAddress( address, format );
expect( formattedAddress.name ).toBe( expected.name );
expect( formattedAddress.address ).toEqual( expected.address );
}
);
} );

View File

@ -1,8 +1,18 @@
/**
* External dependencies
*/
import { LOGIN_URL } from '@woocommerce/block-settings';
import {
ALLOWED_COUNTRIES,
ALLOWED_STATES,
LOGIN_URL,
} from '@woocommerce/block-settings';
import { getSetting } from '@woocommerce/settings';
import {
CartBillingAddress,
CartShippingAddress,
} from '@woocommerce/type-defs/cart';
import { isObject, isString } from '@woocommerce/types';
import { decodeEntities } from '@wordpress/html-entities';
export const LOGIN_TO_CHECKOUT_URL = `${ LOGIN_URL }?redirect_to=${ encodeURIComponent(
window.location.href
@ -12,4 +22,110 @@ export const isLoginRequired = ( customerId: number ): boolean => {
return ! customerId && ! getSetting( 'checkoutAllowsGuest', false );
};
export const getFormattedState = (
address: CartBillingAddress | CartShippingAddress
): string => {
return isObject( ALLOWED_STATES[ address.country ] ) &&
isString( ALLOWED_STATES[ address.country ][ address.state ] )
? decodeEntities( ALLOWED_STATES[ address.country ][ address.state ] )
: address.state;
};
export const getFormattedCountry = (
address: CartBillingAddress | CartShippingAddress
): string => {
return isString( ALLOWED_COUNTRIES[ address.country ] )
? decodeEntities( ALLOWED_COUNTRIES[ address.country ] )
: address.country;
};
/**
* Extract the name format from the parent address format.
*/
export const extractName = ( format: string ): string => {
// Name can be a few different formats:
const nameTokens = [
'{name}',
'{name_upper}',
'{first_name} {last_name}',
'{last_name} {first_name}',
'{first_name_upper} {last_name_upper}',
'{last_name_upper} {first_name_upper}',
'{first_name} {last_name_upper}',
'{first_name_upper} {last_name}',
'{last_name} {first_name_upper}',
'{last_name_upper} {first_name}',
];
return nameTokens.find( ( token ) => format.indexOf( token ) >= 0 ) || '';
};
/**
* Format an address for display in the address card.
*/
export const formatAddress = (
address: CartBillingAddress | CartShippingAddress,
format: string
): { name: string; address: string[] } => {
const nameFormat = extractName( format );
const addressFormatWithoutName = format.replace( `${ nameFormat }\n`, '' );
const addressTokens = [
[ '{company}', address?.company || '' ],
[ '{address_1}', address?.address_1 || '' ],
[ '{address_2}', address?.address_2 || '' ],
[ '{city}', address?.city || '' ],
[ '{state}', getFormattedState( address ) ],
[ '{postcode}', address?.postcode || '' ],
[ '{country}', getFormattedCountry( address ) ],
[ '{company_upper}', ( address?.company || '' ).toUpperCase() ],
[ '{address_1_upper}', ( address?.address_1 || '' ).toUpperCase() ],
[ '{address_2_upper}', ( address?.address_2 || '' ).toUpperCase() ],
[ '{city_upper}', ( address?.city || '' ).toUpperCase() ],
[ '{state_upper}', getFormattedState( address ).toUpperCase() ],
[ '{state_code}', address?.state || '' ],
[ '{postcode_upper}', ( address?.postcode || '' ).toUpperCase() ],
[ '{country_upper}', getFormattedCountry( address ).toUpperCase() ],
];
const nameTokens = [
[
'{name}',
address?.first_name +
// Only include the space if the first name was present.
( address?.first_name && address?.last_name ? ' ' : '' ) +
address?.last_name,
],
[
'{name_upper}',
(
address?.first_name +
// Only include the space if the first name was present.
( address?.first_name && address?.last_name ? ' ' : '' ) +
address?.last_name
).toUpperCase(),
],
[ '{first_name}', address?.first_name || '' ],
[ '{last_name}', address?.last_name || '' ],
[ '{first_name_upper}', ( address?.first_name || '' ).toUpperCase() ],
[ '{last_name_upper}', ( address?.last_name || '' ).toUpperCase() ],
];
let parsedName = nameFormat;
nameTokens.forEach( ( [ token, value ] ) => {
parsedName = parsedName.replace( token, value );
} );
let parsedAddress = addressFormatWithoutName;
addressTokens.forEach( ( [ token, value ] ) => {
parsedAddress = parsedAddress.replace( token, value );
} );
const addressParts = parsedAddress
.replace( /^,\s|,\s$/g, '' )
.replace( /\n{2,}/, '\n' )
.split( '\n' )
.filter( Boolean );
return { name: parsedName, address: addressParts };
};
export const reloadPage = (): void => void window.location.reload( true );

View File

@ -1,11 +1,8 @@
/**
* External dependencies
*/
import {
getSetting,
STORE_PAGES,
LocaleSpecificFormField,
} from '@woocommerce/settings';
import { getSetting, STORE_PAGES } from '@woocommerce/settings';
import { CountryData } from '@woocommerce/types';
export type WordCountType =
| 'words'
@ -51,13 +48,6 @@ export const LOCAL_PICKUP_ENABLED = getSetting< boolean >(
false
);
type CountryData = {
allowBilling: boolean;
allowShipping: boolean;
states: Record< string, string >;
locale: Record< string, LocaleSpecificFormField >;
};
type FieldsLocations = {
address: string[];
contact: string[];

View File

@ -0,0 +1,12 @@
/**
* External dependencies
*/
import { LocaleSpecificFormField } from '@woocommerce/settings';
export type CountryData = {
allowBilling: boolean;
allowShipping: boolean;
states: Record< string, string >;
locale: Record< string, LocaleSpecificFormField >;
format?: string;
};

View File

@ -6,6 +6,7 @@ export * from './cart-response';
export * from './cart';
export * from './checkout';
export * from './contexts';
export * from './countries';
export * from './currency';
export * from './events';
export * from './hocs';

View File

@ -4,6 +4,12 @@ require( '@wordpress/data' );
// wcSettings is required by @woocommerce/* packages
global.wcSettings = {
adminUrl: 'https://vagrant.local/wp/wp-admin/',
addressFormats: {
default:
'{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}',
JP: '{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}',
CA: '{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}',
},
shippingMethodsExist: true,
currency: {
code: 'USD',
@ -45,6 +51,7 @@ global.wcSettings = {
postcode: { priority: 65 },
state: { required: false, hidden: true },
},
format: '{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}',
},
CA: {
states: {
@ -56,6 +63,29 @@ global.wcSettings = {
postcode: { label: 'Postal code' },
state: { label: 'Province' },
},
format: '{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}',
},
JP: {
allowBilling: true,
allowShipping: true,
states: {
JP28: 'Hyogo',
},
locale: {
last_name: { priority: 10 },
first_name: { priority: 20 },
postcode: {
priority: 65,
},
state: {
label: 'Prefecture',
priority: 66,
},
city: { priority: 67 },
address_1: { priority: 68 },
address_2: { priority: 69 },
},
format: '{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}',
},
GB: {
states: {},

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Use the address formats from WC_Countries in the checkout block address card

View File

@ -321,7 +321,20 @@ class Checkout extends AbstractBlock {
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'countryData', CartCheckoutUtils::get_country_data(), true );
$country_data = CartCheckoutUtils::get_country_data();
$address_formats = WC()->countries->get_address_formats();
// Move the address format into the 'countryData' setting.
// We need to skip 'default' because that's not a valid country.
foreach ( $address_formats as $country_code => $format ) {
if ( 'default' === $country_code ) {
continue;
}
$country_data[ $country_code ]['format'] = $format;
}
$this->asset_data_registry->add( 'countryData', $country_data, true );
$this->asset_data_registry->add( 'defaultAddressFormat', $address_formats['default'], true );
$this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true );
$this->asset_data_registry->add(
'checkoutAllowsGuest',