From 591f4d90b9ab5ac1f91c8bc83fa128c2ee7e3c05 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 22 Feb 2023 15:51:20 +0700 Subject: [PATCH 01/13] Add client side postcode validation --- .../address-form/address-form.tsx | 42 ++++- assets/js/base/components/combobox/index.tsx | 2 +- .../components/state-input/StateInputProps.ts | 2 +- package-lock.json | 11 ++ package.json | 1 + .../validation/getValidityMessageForInput.ts | 37 ++++ packages/checkout/utils/validation/index.ts | 61 +----- .../checkout/utils/validation/isPostcode.ts | 31 +++ .../checkout/utils/validation/mustContain.ts | 26 +++ .../utils/validation/test/isPostcode.ts | 177 ++++++++++++++++++ 10 files changed, 329 insertions(+), 61 deletions(-) create mode 100644 packages/checkout/utils/validation/getValidityMessageForInput.ts create mode 100644 packages/checkout/utils/validation/isPostcode.ts create mode 100644 packages/checkout/utils/validation/mustContain.ts create mode 100644 packages/checkout/utils/validation/test/isPostcode.ts diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 3a211ee37fe..07058a67b9d 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { ValidatedTextInput } from '@woocommerce/blocks-checkout'; +import { ValidatedTextInput, isPostcode } from '@woocommerce/blocks-checkout'; import { BillingCountryInput, ShippingCountryInput, @@ -214,6 +214,46 @@ const AddressForm = ( { ); } + const customValidationHandler = ( + inputObject: HTMLInputElement + ) => { + if ( + ! isPostcode( { + postcode: values.postcode, + country: values.country, + } ) + ) { + inputObject.setCustomValidity( + __( + 'Please provide a valid postcode', + 'woo-gutenberg-products-block' + ) + ); + return false; + } + return true; + }; + + if ( field.key === 'postcode' ) { + return ( + + onChange( { + ...values, + [ field.key ]: newValue, + } ) + } + customValidation={ customValidationHandler } + errorMessage={ field.errorMessage } + /> + ); + } + return ( void; required?: boolean; - errorMessage?: string; + errorMessage?: string | undefined; } export type StateInputWithStatesProps = StateInputProps & { diff --git a/package-lock.json b/package-lock.json index b725b659e3c..9171174df3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "dompurify": "^2.4.0", "downshift": "6.1.7", "html-react-parser": "3.0.4", + "postcode-validator": "3.7.0", "preact": "^10.11.3", "react-number-format": "4.9.3", "reakit": "1.3.11", @@ -39835,6 +39836,11 @@ "node": ">=0.10.0" } }, + "node_modules/postcode-validator": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/postcode-validator/-/postcode-validator-3.7.0.tgz", + "integrity": "sha512-pl697wPxQ8kb3S0qoIHdNsqPjMY3ieKQtR6+dp0o+NOlM6ImzDpyKSdlXgGzzjoUElDUdXWezFjGGd9yITM+Xg==" + }, "node_modules/postcss": { "version": "8.4.14", "dev": true, @@ -78122,6 +78128,11 @@ "version": "0.1.1", "dev": true }, + "postcode-validator": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/postcode-validator/-/postcode-validator-3.7.0.tgz", + "integrity": "sha512-pl697wPxQ8kb3S0qoIHdNsqPjMY3ieKQtR6+dp0o+NOlM6ImzDpyKSdlXgGzzjoUElDUdXWezFjGGd9yITM+Xg==" + }, "postcss": { "version": "8.4.14", "dev": true, diff --git a/package.json b/package.json index 03f1084768f..facb022f755 100644 --- a/package.json +++ b/package.json @@ -251,6 +251,7 @@ "dompurify": "^2.4.0", "downshift": "6.1.7", "html-react-parser": "3.0.4", + "postcode-validator": "3.7.0", "preact": "^10.11.3", "react-number-format": "4.9.3", "reakit": "1.3.11", diff --git a/packages/checkout/utils/validation/getValidityMessageForInput.ts b/packages/checkout/utils/validation/getValidityMessageForInput.ts new file mode 100644 index 00000000000..36616c59304 --- /dev/null +++ b/packages/checkout/utils/validation/getValidityMessageForInput.ts @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Converts an input's validityState to a string to display on the frontend. + * + * This returns custom messages for invalid/required fields. Other error types use defaults from the browser (these + * could be implemented in the future but are not currently used by the block checkout). + */ +const getValidityMessageForInput = ( + label: string, + inputElement: HTMLInputElement +): string => { + const { valid, customError, valueMissing, badInput, typeMismatch } = + inputElement.validity; + + // No errors, or custom error - return early. + if ( valid || customError ) { + return inputElement.validationMessage; + } + + const invalidFieldMessage = sprintf( + /* translators: %s field label */ + __( 'Please enter a valid %s', 'woo-gutenberg-products-block' ), + label.toLowerCase() + ); + + if ( valueMissing || badInput || typeMismatch ) { + return invalidFieldMessage; + } + + return inputElement.validationMessage || invalidFieldMessage; +}; + +export default getValidityMessageForInput; diff --git a/packages/checkout/utils/validation/index.ts b/packages/checkout/utils/validation/index.ts index 1d4e4212c7a..cd38139daad 100644 --- a/packages/checkout/utils/validation/index.ts +++ b/packages/checkout/utils/validation/index.ts @@ -1,58 +1,3 @@ -/** - * External dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Ensures that a given value contains a string, or throws an error. - */ -export const mustContain = ( - value: string, - requiredValue: string -): true | never => { - if ( ! value.includes( requiredValue ) ) { - throw Error( - sprintf( - /* translators: %1$s value passed to filter, %2$s : value that must be included. */ - __( - 'Returned value must include %1$s, you passed "%2$s"', - 'woo-gutenberg-products-block' - ), - requiredValue, - value - ) - ); - } - return true; -}; - -/** - * Converts an input's validityState to a string to display on the frontend. - * - * This returns custom messages for invalid/required fields. Other error types use defaults from the browser (these - * could be implemented in the future but are not currently used by the block checkout). - */ -export const getValidityMessageForInput = ( - label: string, - inputElement: HTMLInputElement -): string => { - const { valid, customError, valueMissing, badInput, typeMismatch } = - inputElement.validity; - - // No errors, or custom error - return early. - if ( valid || customError ) { - return inputElement.validationMessage; - } - - const invalidFieldMessage = sprintf( - /* translators: %s field label */ - __( 'Please enter a valid %s', 'woo-gutenberg-products-block' ), - label.toLowerCase() - ); - - if ( valueMissing || badInput || typeMismatch ) { - return invalidFieldMessage; - } - - return inputElement.validationMessage || invalidFieldMessage; -}; +export { default as mustContain } from './mustContain'; +export { default as getValidityMessageForInput } from './getValidityMessageForInput'; +export { default as isPostcode } from './isPostcode'; diff --git a/packages/checkout/utils/validation/isPostcode.ts b/packages/checkout/utils/validation/isPostcode.ts new file mode 100644 index 00000000000..db78472008e --- /dev/null +++ b/packages/checkout/utils/validation/isPostcode.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { POSTCODE_REGEXES } from 'postcode-validator/lib/cjs/postcode-regexes.js'; + +const getCustomRegexes = () => { + POSTCODE_REGEXES.set( 'BA', /^([7-8]{1})([0-9]{4})$/ ); + POSTCODE_REGEXES.set( + 'GB', + /^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i + ); + POSTCODE_REGEXES.set( 'IN', /^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/ ); + POSTCODE_REGEXES.set( 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ); + POSTCODE_REGEXES.set( 'LI', /^(94[8-9][0-9])$/ ); + POSTCODE_REGEXES.set( 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ); + POSTCODE_REGEXES.set( 'SI', /^([1-9][0-9]{3})$/ ); + + return POSTCODE_REGEXES; +}; +export interface IsPostcodeProps { + postcode: string; + country: string; +} + +const isPostcode = ( { postcode, country }: IsPostcodeProps ) => { + const CUSTOM_POSTCODE_REGEXES = getCustomRegexes(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return CUSTOM_POSTCODE_REGEXES.get( country )!.test( postcode ); +}; + +export default isPostcode; diff --git a/packages/checkout/utils/validation/mustContain.ts b/packages/checkout/utils/validation/mustContain.ts new file mode 100644 index 00000000000..c50ed227ce1 --- /dev/null +++ b/packages/checkout/utils/validation/mustContain.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Ensures that a given value contains a string, or throws an error. + */ +const mustContain = ( value: string, requiredValue: string ): true | never => { + if ( ! value.includes( requiredValue ) ) { + throw Error( + sprintf( + /* translators: %1$s value passed to filter, %2$s : value that must be included. */ + __( + 'Returned value must include %1$s, you passed "%2$s"', + 'woo-gutenberg-products-block' + ), + requiredValue, + value + ) + ); + } + return true; +}; + +export default mustContain; diff --git a/packages/checkout/utils/validation/test/isPostcode.ts b/packages/checkout/utils/validation/test/isPostcode.ts new file mode 100644 index 00000000000..5126333aac3 --- /dev/null +++ b/packages/checkout/utils/validation/test/isPostcode.ts @@ -0,0 +1,177 @@ +/** + * Internal dependencies + */ +import isPostcode from '../isPostcode'; +import type { IsPostcodeProps } from '../isPostcode'; + +describe( 'isPostcode', () => { + const cases = [ + // Austrian postcodes + [ true, '1000', 'AT' ], + [ true, '9999', 'AT' ], + [ false, '0000', 'AT' ], + [ false, '10000', 'AT' ], + + // Bosnian postcodes + [ true, '71000', 'BA' ], + [ true, '78256', 'BA' ], + [ true, '89240', 'BA' ], + [ false, '61000', 'BA' ], + [ false, '7850', 'BA' ], + + // Belgian postcodes + [ true, '1111', 'BE' ], + [ false, '111', 'BE' ], + [ false, '11111', 'BE' ], + + // Brazilian postcodes + [ true, '99999-999', 'BR' ], + [ true, '99999999', 'BR' ], + [ false, '99999 999', 'BR' ], + [ false, '99999-ABC', 'BR' ], + + // Canadian postcodes + [ true, 'A9A 9A9', 'CA' ], + [ true, 'A9A9A9', 'CA' ], + [ true, 'a9a9a9', 'CA' ], + [ false, 'D0A 9A9', 'CA' ], + [ false, '99999', 'CA' ], + [ false, 'ABC999', 'CA' ], + [ false, '0A0A0A', 'CA' ], + + // Swiss postcodes + [ true, '9999', 'CH' ], + [ false, '99999', 'CH' ], + [ false, 'ABCDE', 'CH' ], + + // Czech postcodes + [ true, '160 00', 'CZ' ], + [ true, '16000', 'CZ' ], + [ false, '1600', 'CZ' ], + + // German postcodes + [ true, '01234', 'DE' ], + [ true, '12345', 'DE' ], + [ false, '12 345', 'DE' ], + [ false, '1234', 'DE' ], + + // Spanish postcodes + [ true, '03000', 'ES' ], + [ true, '08000', 'ES' ], + [ false, '08 000', 'ES' ], + [ false, '1234', 'ES' ], + + // French postcodes + [ true, '01000', 'FR' ], + [ true, '99999', 'FR' ], + [ true, '01 000', 'FR' ], + [ false, '1234', 'FR' ], + + // British postcodes + [ true, 'AA9A 9AA', 'GB' ], + [ true, 'A9A 9AA', 'GB' ], + [ true, 'A9 9AA', 'GB' ], + [ true, 'A99 9AA', 'GB' ], + [ true, 'AA99 9AA', 'GB' ], + [ true, 'BFPO 801', 'GB' ], + [ false, '99999', 'GB' ], + [ false, '9999 999', 'GB' ], + [ false, '999 999', 'GB' ], + [ false, '99 999', 'GB' ], + [ false, '9A A9A', 'GB' ], + + // Hungarian postcodes + [ true, '1234', 'HU' ], + [ false, '123', 'HU' ], + [ false, '12345', 'HU' ], + + // Irish postcodes + [ true, 'A65F4E2', 'IE' ], + [ true, 'A65 F4E2', 'IE' ], + [ true, 'A65-F4E2', 'IE' ], + [ false, 'B23F854', 'IE' ], + + // Indian postcodes + [ true, '110001', 'IN' ], + [ true, '110 001', 'IN' ], + [ false, '11 0001', 'IN' ], + [ false, '1100 01', 'IN' ], + + // Italian postcodes + [ true, '99999', 'IT' ], + [ false, '9999', 'IT' ], + [ false, 'ABC 999', 'IT' ], + [ false, 'ABC-999', 'IT' ], + [ false, 'ABC_123', 'IT' ], + + // Japanese postcodes + [ true, '1340088', 'JP' ], + [ true, '134-0088', 'JP' ], + [ false, '1340-088', 'JP' ], + [ false, '12345', 'JP' ], + [ false, '0123', 'JP' ], + + // Lichtenstein postcodes + [ true, '9485', 'LI' ], + [ true, '9486', 'LI' ], + [ true, '9499', 'LI' ], + [ false, '9585', 'LI' ], + [ false, '9385', 'LI' ], + [ false, '9475', 'LI' ], + + // Dutch postcodes + [ true, '3852GC', 'NL' ], + [ true, '3852 GC', 'NL' ], + [ true, '3852 gc', 'NL' ], + [ false, '3852SA', 'NL' ], + [ false, '3852 SA', 'NL' ], + [ false, '3852 sa', 'NL' ], + + // Polish postcodes + [ true, '00-001', 'PL' ], + [ true, '99-440', 'PL' ], + [ false, '000-01', 'PL' ], + [ false, '994-40', 'PL' ], + [ false, '00001', 'PL' ], + [ false, '99440', 'PL' ], + + // Puerto Rican postcodes + [ true, '00901', 'PR' ], + [ true, '00617', 'PR' ], + [ true, '00602-1211', 'PR' ], + [ false, '1234', 'PR' ], + [ false, '0060-21211', 'PR' ], + + // Portuguese postcodes + [ true, '1234-567', 'PT' ], + [ true, '2345-678', 'PT' ], + [ false, '123-4567', 'PT' ], + [ false, '234-5678', 'PT' ], + + // Slovenian postcodes + [ true, '1234', 'SI' ], + [ true, '1000', 'SI' ], + [ true, '9876', 'SI' ], + [ false, '12345', 'SI' ], + [ false, '0123', 'SI' ], + + // Slovak postcodes + [ true, '010 01', 'SK' ], + [ true, '01001', 'SK' ], + [ false, '01 001', 'SK' ], + [ false, '1234', 'SK' ], + [ false, '123456', 'SK' ], + + // United States postcodes + [ true, '90210', 'US' ], + [ true, '99577-0727', 'US' ], + [ false, 'ABCDE', 'US' ], + [ false, 'ABCDE-9999', 'US' ], + ]; + + test.each( cases )( '%s: %s for %s', ( result, postcode, country ) => + expect( isPostcode( { postcode, country } as IsPostcodeProps ) ).toBe( + result + ) + ); +} ); From 4c53bb8f0c492c52ddd16537282dc7378f5f2865 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 22 Feb 2023 16:55:34 +0700 Subject: [PATCH 02/13] Prevent server-side validation --- .../base/components/cart-checkout/address-form/address-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 07058a67b9d..b6beeed4f64 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -239,6 +239,7 @@ const AddressForm = ( { Date: Thu, 23 Feb 2023 12:11:29 +0700 Subject: [PATCH 03/13] Adjust translation --- .../base/components/cart-checkout/address-form/address-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index b6beeed4f64..595fef52fd5 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -225,7 +225,7 @@ const AddressForm = ( { ) { inputObject.setCustomValidity( __( - 'Please provide a valid postcode', + 'Please enter a valid postcode', 'woo-gutenberg-products-block' ) ); From 779a51c86f55f2c0c78516821444f395c07c38cc Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Thu, 23 Feb 2023 12:18:46 +0700 Subject: [PATCH 04/13] Only validate postcode if country is available --- .../base/components/cart-checkout/address-form/address-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 595fef52fd5..a37f166c35e 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -218,6 +218,7 @@ const AddressForm = ( { inputObject: HTMLInputElement ) => { if ( + values.country && ! isPostcode( { postcode: values.postcode, country: values.country, From fb437fd0509525f3ff4fa4d55a65cfd6ae142888 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Thu, 23 Feb 2023 12:45:27 +0700 Subject: [PATCH 05/13] Specify return type of isPostcode() --- packages/checkout/utils/validation/isPostcode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/checkout/utils/validation/isPostcode.ts b/packages/checkout/utils/validation/isPostcode.ts index db78472008e..9f06255b354 100644 --- a/packages/checkout/utils/validation/isPostcode.ts +++ b/packages/checkout/utils/validation/isPostcode.ts @@ -22,7 +22,7 @@ export interface IsPostcodeProps { country: string; } -const isPostcode = ( { postcode, country }: IsPostcodeProps ) => { +const isPostcode = ( { postcode, country }: IsPostcodeProps ): boolean => { const CUSTOM_POSTCODE_REGEXES = getCustomRegexes(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return CUSTOM_POSTCODE_REGEXES.get( country )!.test( postcode ); From 8028cce88b9d19c90fe1f88c06f0a37a08c17512 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Thu, 23 Feb 2023 14:53:15 +0700 Subject: [PATCH 06/13] Convert function to static variable set --- .../checkout/utils/validation/isPostcode.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/checkout/utils/validation/isPostcode.ts b/packages/checkout/utils/validation/isPostcode.ts index 9f06255b354..f44278cc042 100644 --- a/packages/checkout/utils/validation/isPostcode.ts +++ b/packages/checkout/utils/validation/isPostcode.ts @@ -3,29 +3,32 @@ */ import { POSTCODE_REGEXES } from 'postcode-validator/lib/cjs/postcode-regexes.js'; -const getCustomRegexes = () => { - POSTCODE_REGEXES.set( 'BA', /^([7-8]{1})([0-9]{4})$/ ); - POSTCODE_REGEXES.set( +const CUSTOM_REGEXES = new Map< string, RegExp >( [ + [ 'BA', /^([7-8]{1})([0-9]{4})$/ ], + [ 'GB', - /^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i - ); - POSTCODE_REGEXES.set( 'IN', /^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/ ); - POSTCODE_REGEXES.set( 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ); - POSTCODE_REGEXES.set( 'LI', /^(94[8-9][0-9])$/ ); - POSTCODE_REGEXES.set( 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ); - POSTCODE_REGEXES.set( 'SI', /^([1-9][0-9]{3})$/ ); + /^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i, + ], + [ 'IN', /^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/ ], + [ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ], + [ 'LI', /^(94[8-9][0-9])$/ ], + [ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ], + [ 'SI', /^([1-9][0-9]{3})$/ ], +] ); + +const DEFAULT_REGEXES = new Map< string, RegExp >( [ + ...POSTCODE_REGEXES, + ...CUSTOM_REGEXES, +] ); - return POSTCODE_REGEXES; -}; export interface IsPostcodeProps { postcode: string; country: string; } const isPostcode = ( { postcode, country }: IsPostcodeProps ): boolean => { - const CUSTOM_POSTCODE_REGEXES = getCustomRegexes(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return CUSTOM_POSTCODE_REGEXES.get( country )!.test( postcode ); + return DEFAULT_REGEXES.get( country )!.test( postcode ); }; export default isPostcode; From f7c6cc1645b32b29f1a99762ab9e771313765c4a Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Fri, 24 Feb 2023 15:18:25 +0700 Subject: [PATCH 07/13] Refactor for postcode validation --- .../address-form/address-form.tsx | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index a37f166c35e..cec368a6c2c 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -218,6 +218,7 @@ const AddressForm = ( { inputObject: HTMLInputElement ) => { if ( + field.key === 'postcode' && values.country && ! isPostcode( { postcode: values.postcode, @@ -235,27 +236,6 @@ const AddressForm = ( { return true; }; - if ( field.key === 'postcode' ) { - return ( - - onChange( { - ...values, - [ field.key ]: newValue, - } ) - } - customValidation={ customValidationHandler } - errorMessage={ field.errorMessage } - /> - ); - } - return ( From 59be85f907c6cc3f6fcc9d968f3b8fdf2fa6dd78 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Fri, 24 Feb 2023 18:05:29 +0700 Subject: [PATCH 08/13] Refactor customValidationHandler --- .../address-form/address-form.tsx | 46 ++++++++++--------- .../text-input/validated-text-input.tsx | 8 +++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index cec368a6c2c..4ccd766bd0c 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -144,6 +144,29 @@ const AddressForm = ( { id = id || instanceId; + const customValidationHandler = ( + inputObject: HTMLInputElement, + fieldName: string + ) => { + if ( + fieldName === 'postcode' && + values.country && + ! isPostcode( { + postcode: values.postcode, + country: values.country, + } ) + ) { + inputObject.setCustomValidity( + __( + 'Please enter a valid postcode', + 'woo-gutenberg-products-block' + ) + ); + return false; + } + return true; + }; + return (
{ addressFormFields.map( ( field ) => { @@ -214,32 +237,11 @@ const AddressForm = ( { ); } - const customValidationHandler = ( - inputObject: HTMLInputElement - ) => { - if ( - field.key === 'postcode' && - values.country && - ! isPostcode( { - postcode: values.postcode, - country: values.country, - } ) - ) { - inputObject.setCustomValidity( - __( - 'Please enter a valid postcode', - 'woo-gutenberg-products-block' - ) - ); - return false; - } - return true; - }; - return ( boolean ) + | ( ( inputObject: HTMLInputElement, fieldName: string ) => boolean ) | undefined; + fieldName: string; } const ValidatedTextInput = ( { @@ -69,6 +70,7 @@ const ValidatedTextInput = ( { value = '', customValidation, label, + fieldName, ...rest }: ValidatedTextInputProps ): JSX.Element => { const [ isPristine, setIsPristine ] = useState( true ); @@ -106,7 +108,8 @@ const ValidatedTextInput = ( { } const inputIsValid = customValidation - ? inputObject.checkValidity() && customValidation( inputObject ) + ? inputObject.checkValidity() && + customValidation( inputObject, fieldName ) : inputObject.checkValidity(); if ( inputIsValid ) { @@ -130,6 +133,7 @@ const ValidatedTextInput = ( { errorIdString, setValidationErrors, label, + fieldName, ] ); From b6dca6ff36ebb123e01f044f10c5b84d70d1de1b Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Tue, 7 Mar 2023 12:16:35 +0700 Subject: [PATCH 09/13] Use customValidationHandler as intermediate function --- .../address-form/address-form.tsx | 31 ++++++++++++++----- .../text-input/validated-text-input.tsx | 8 ++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 4ccd766bd0c..7bace79f0b4 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -144,16 +144,26 @@ const AddressForm = ( { id = id || instanceId; - const customValidationHandler = ( + type customValidationHandlerType = ( inputObject: HTMLInputElement, - fieldName: string + field: string, + customValues: { + country: string; + postcode: string; + } + ) => boolean; + + const customValidationHandler: customValidationHandlerType = ( + inputObject, + field, + customValues ) => { if ( - fieldName === 'postcode' && - values.country && + field === 'postcode' && + customValues.country && ! isPostcode( { - postcode: values.postcode, - country: values.country, + postcode: customValues.postcode, + country: customValues.country, } ) ) { inputObject.setCustomValidity( @@ -241,7 +251,6 @@ const AddressForm = ( { + customValidationHandler( + inputObject, + field.key, + values + ) + } errorMessage={ field.errorMessage } required={ field.required } /> diff --git a/packages/checkout/components/text-input/validated-text-input.tsx b/packages/checkout/components/text-input/validated-text-input.tsx index 90e957894b7..c196cb5b407 100644 --- a/packages/checkout/components/text-input/validated-text-input.tsx +++ b/packages/checkout/components/text-input/validated-text-input.tsx @@ -47,9 +47,8 @@ interface ValidatedTextInputProps errorMessage?: string | undefined; // Custom validation function that is run on change. Use setCustomValidity to set an error message. customValidation?: - | ( ( inputObject: HTMLInputElement, fieldName: string ) => boolean ) + | ( ( inputObject: HTMLInputElement ) => boolean ) | undefined; - fieldName: string; } const ValidatedTextInput = ( { @@ -65,7 +64,6 @@ const ValidatedTextInput = ( { value = '', customValidation, label, - fieldName, ...rest }: ValidatedTextInputProps ): JSX.Element => { const [ isPristine, setIsPristine ] = useState( true ); @@ -103,8 +101,7 @@ const ValidatedTextInput = ( { } const inputIsValid = customValidation - ? inputObject.checkValidity() && - customValidation( inputObject, fieldName ) + ? inputObject.checkValidity() && customValidation( inputObject ) : inputObject.checkValidity(); if ( inputIsValid ) { @@ -128,7 +125,6 @@ const ValidatedTextInput = ( { errorIdString, setValidationErrors, label, - fieldName, ] ); From 0902665c0a810d6e0476fae4816811b06d0d3ef7 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 8 Mar 2023 09:22:49 +0700 Subject: [PATCH 10/13] Hyphenate file names --- ...MessageForInput.ts => get-validity-message-for-input.ts} | 0 packages/checkout/utils/validation/index.ts | 6 +++--- .../utils/validation/{isPostcode.ts => is-postcode.ts} | 0 .../utils/validation/{mustContain.ts => must-contain.ts} | 0 .../utils/validation/test/{isPostcode.ts => is-postcode.ts} | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename packages/checkout/utils/validation/{getValidityMessageForInput.ts => get-validity-message-for-input.ts} (100%) rename packages/checkout/utils/validation/{isPostcode.ts => is-postcode.ts} (100%) rename packages/checkout/utils/validation/{mustContain.ts => must-contain.ts} (100%) rename packages/checkout/utils/validation/test/{isPostcode.ts => is-postcode.ts} (97%) diff --git a/packages/checkout/utils/validation/getValidityMessageForInput.ts b/packages/checkout/utils/validation/get-validity-message-for-input.ts similarity index 100% rename from packages/checkout/utils/validation/getValidityMessageForInput.ts rename to packages/checkout/utils/validation/get-validity-message-for-input.ts diff --git a/packages/checkout/utils/validation/index.ts b/packages/checkout/utils/validation/index.ts index cd38139daad..8dcafa3d869 100644 --- a/packages/checkout/utils/validation/index.ts +++ b/packages/checkout/utils/validation/index.ts @@ -1,3 +1,3 @@ -export { default as mustContain } from './mustContain'; -export { default as getValidityMessageForInput } from './getValidityMessageForInput'; -export { default as isPostcode } from './isPostcode'; +export { default as mustContain } from './must-contain'; +export { default as getValidityMessageForInput } from './get-validity-message-for-input'; +export { default as isPostcode } from './is-postcode'; diff --git a/packages/checkout/utils/validation/isPostcode.ts b/packages/checkout/utils/validation/is-postcode.ts similarity index 100% rename from packages/checkout/utils/validation/isPostcode.ts rename to packages/checkout/utils/validation/is-postcode.ts diff --git a/packages/checkout/utils/validation/mustContain.ts b/packages/checkout/utils/validation/must-contain.ts similarity index 100% rename from packages/checkout/utils/validation/mustContain.ts rename to packages/checkout/utils/validation/must-contain.ts diff --git a/packages/checkout/utils/validation/test/isPostcode.ts b/packages/checkout/utils/validation/test/is-postcode.ts similarity index 97% rename from packages/checkout/utils/validation/test/isPostcode.ts rename to packages/checkout/utils/validation/test/is-postcode.ts index 5126333aac3..eff2d6efe5d 100644 --- a/packages/checkout/utils/validation/test/isPostcode.ts +++ b/packages/checkout/utils/validation/test/is-postcode.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import isPostcode from '../isPostcode'; -import type { IsPostcodeProps } from '../isPostcode'; +import isPostcode from '../is-postcode'; +import type { IsPostcodeProps } from '../is-postcode'; describe( 'isPostcode', () => { const cases = [ From 794576035dd3643a10b366e9e0158140d537a965 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 8 Mar 2023 22:00:17 +0700 Subject: [PATCH 11/13] Update packages/checkout/utils/validation/is-postcode.ts Co-authored-by: Mike Jolley --- packages/checkout/utils/validation/is-postcode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/checkout/utils/validation/is-postcode.ts b/packages/checkout/utils/validation/is-postcode.ts index f44278cc042..82ff5606916 100644 --- a/packages/checkout/utils/validation/is-postcode.ts +++ b/packages/checkout/utils/validation/is-postcode.ts @@ -7,7 +7,7 @@ const CUSTOM_REGEXES = new Map< string, RegExp >( [ [ 'BA', /^([7-8]{1})([0-9]{4})$/ ], [ 'GB', - /^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i, + /^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-Z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i, ], [ 'IN', /^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/ ], [ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ], From c8495728e1253a95cb93487bef4c3e926ee29bf4 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Fri, 10 Mar 2023 12:42:04 +0700 Subject: [PATCH 12/13] Normalise postcode on input --- .../components/cart-checkout/address-form/address-form.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 7bace79f0b4..577feea5d60 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -262,7 +262,10 @@ const AddressForm = ( { onChange={ ( newValue: string ) => onChange( { ...values, - [ field.key ]: newValue, + [ field.key ]: + field.key === 'postcode' + ? newValue.trimStart().toUpperCase() + : newValue, } ) } customValidation={ ( inputObject: HTMLInputElement ) => From c29f8b8d094cbbed01494f606a162763545d1472 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 14 Mar 2023 10:19:48 +0000 Subject: [PATCH 13/13] Fix usage of out of date value from input field --- .../cart-checkout/address-form/address-form.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index 577feea5d60..def20c9943a 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -144,25 +144,21 @@ const AddressForm = ( { id = id || instanceId; - type customValidationHandlerType = ( + /** + * Custom validation handler for fields with field specific handling. + */ + const customValidationHandler = ( inputObject: HTMLInputElement, field: string, customValues: { country: string; - postcode: string; } - ) => boolean; - - const customValidationHandler: customValidationHandlerType = ( - inputObject, - field, - customValues - ) => { + ): boolean => { if ( field === 'postcode' && customValues.country && ! isPostcode( { - postcode: customValues.postcode, + postcode: inputObject.value, country: customValues.country, } ) ) {