From 74b3dcf96b7a4ffb91ef2cdb3014e3f4657173ad Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 22 Feb 2023 14:13:18 +0700 Subject: [PATCH 1/3] 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 fb7d8749b226399b8f8678ea96c3be96bf09a0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 22 Feb 2023 08:32:56 +0100 Subject: [PATCH 2/3] Update Product Details block so it inherits more styles from the theme (#8494) * Use wc-tabs selectors instead of tabs as WC core does * Remove hardcoded colors from Product Details tabs --- .../blocks/product-elements/product-details/block.tsx | 2 +- .../blocks/product-elements/product-details/style.scss | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/assets/js/atomic/blocks/product-elements/product-details/block.tsx b/assets/js/atomic/blocks/product-elements/product-details/block.tsx index 37db65931f0..74c46267b3a 100644 --- a/assets/js/atomic/blocks/product-elements/product-details/block.tsx +++ b/assets/js/atomic/blocks/product-elements/product-details/block.tsx @@ -84,7 +84,7 @@ export const SingleProductDetails = () => { return (
-
    +
      { tabsTitle }
    { tabsContent } diff --git a/assets/js/atomic/blocks/product-elements/product-details/style.scss b/assets/js/atomic/blocks/product-elements/product-details/style.scss index 07b5c8031f0..9e20b096c78 100644 --- a/assets/js/atomic/blocks/product-elements/product-details/style.scss +++ b/assets/js/atomic/blocks/product-elements/product-details/style.scss @@ -1,5 +1,5 @@ .wp-block-woocommerce-product-details { - ul.tabs { + ul.wc-tabs { list-style: none; padding: 0 0 0 1em; margin: 0 0 1.618em; @@ -9,35 +9,27 @@ li { border: 1px solid $gray-200; - background-color: $white; display: inline-block; position: relative; z-index: 0; border-radius: 4px 4px 0 0; margin: 0; padding: 0.5em 1em; - opacity: 0.5; a { display: inline-block; font-weight: 700; - color: $black; text-decoration: none; &:hover { text-decoration: none; - color: color.adjust($black, $lightness: 10%); } } &.active { - background: $gray-100; z-index: 2; - border-bottom-color: $gray-200; - opacity: 1; a { - color: inherit; text-shadow: inherit; } } From 5b85d318490793511d2db598dc70637327e7fd21 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 22 Feb 2023 14:13:18 +0700 Subject: [PATCH 3/3] 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 + ) + ); +} );