Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add client side postcode validation #8503

Merged
merged 28 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
591f4d9
Add client side postcode validation
nielslange Feb 22, 2023
4c53bb8
Prevent server-side validation
nielslange Feb 22, 2023
4dbfb7c
Adjust translation
nielslange Feb 23, 2023
779a51c
Only validate postcode if country is available
nielslange Feb 23, 2023
fb437fd
Specify return type of isPostcode()
nielslange Feb 23, 2023
8028cce
Convert function to static variable set
nielslange Feb 23, 2023
e1b4470
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Feb 23, 2023
711bddd
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Feb 24, 2023
f7c6cc1
Refactor <ValidatedTextInput> for postcode validation
nielslange Feb 24, 2023
59be85f
Refactor customValidationHandler
nielslange Feb 24, 2023
da07670
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Feb 27, 2023
08a1b63
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 1, 2023
a1b0146
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 2, 2023
c754683
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 6, 2023
d9d17fb
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 6, 2023
a6077cd
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 7, 2023
b6dca6f
Use customValidationHandler as intermediate function
nielslange Mar 7, 2023
c543789
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 7, 2023
6db40a8
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 8, 2023
0902665
Hyphenate file names
nielslange Mar 8, 2023
36eb53b
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 8, 2023
7945760
Update packages/checkout/utils/validation/is-postcode.ts
nielslange Mar 8, 2023
4b39785
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 8, 2023
370263f
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 9, 2023
c849572
Normalise postcode on input
nielslange Mar 10, 2023
de8ee24
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 10, 2023
3cc951b
Merge branch 'trunk' into add/7722-add-client-side-postcode-validation
nielslange Mar 14, 2023
c29f8b8
Fix usage of out of date value from input field
mikejolley Mar 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
import { ValidatedTextInput, isPostcode } from '@woocommerce/blocks-checkout';
import {
BillingCountryInput,
ShippingCountryInput,
Expand Down Expand Up @@ -214,6 +214,47 @@ const AddressForm = ( {
);
}

const customValidationHandler = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense for this customValidationHandler to live here alongside other field types. It's only used by the postcode field. If you look at the ValidatedTextInput below there is also quite a bit of duplication I feel could be refactored more cleanly.

How about defining this elsewhere (outside of the AddressForm component/utility) and add this to the main component being returned so that customValidation is either postcode validation (if the field key is postcode) or undefined for other types?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So like a general validation handler?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something to improve the convenience because if we move to modal, the local state needs to be used over the data store.

inputObject: HTMLInputElement
) => {
if (
! isPostcode( {
postcode: values.postcode,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using values state because inputObject.value only gives us postcode?

I wonder if the rest of the address state is something we should pass to customValidationHandler to make this type of validation easier. cc @senadir ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure, I think it make sense to only inputObject for now, as long as we're not using the address is some comparison or event tracking then it's fine for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using values state because inputObject.value only gives us postcode?

I was using values as the callback function for customValidation expects an HTMLInputElement as input.

I wonder if the rest of the address state is something we should pass to customValidationHandler to make this type of validation easier.

Within customValidationHandler(), values holds the following object:

{
    "first_name": "",
    "last_name": "",
    "company": "",
    "address_1": "",
    "address_2": "",
    "city": "111",
    "state": "",
    "postcode": "",
    "country": "",
    "phone": ""
}

Is that what you meant?

country: values.country,
nielslange marked this conversation as resolved.
Show resolved Hide resolved
} )
) {
inputObject.setCustomValidity(
__(
'Please provide a valid postcode',
nielslange marked this conversation as resolved.
Show resolved Hide resolved
'woo-gutenberg-products-block'
)
);
return false;
}
return true;
};

if ( field.key === 'postcode' ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comments. I think we can just adjust the existing ValidatedTextInput below this block of code rather than redefine it for postcode. All that changes is the customValidation.

return (
<ValidatedTextInput
id={ `${ id }-${ field.key }` }
key={ `${ id }-${ field.key }` }
errorId={ errorId }
value={ values.postcode }
label={ field.label }
required={ field.required }
onChange={ ( newValue: string ) =>
onChange( {
...values,
[ field.key ]: newValue,
} )
}
customValidation={ customValidationHandler }
errorMessage={ field.errorMessage }
/>
);
}

return (
<ValidatedTextInput
key={ field.key }
Expand Down
2 changes: 1 addition & 1 deletion assets/js/base/components/combobox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface ComboboxProps {
autoComplete?: string;
className?: string;
errorId: string | null;
errorMessage?: string;
errorMessage?: string | undefined;
nielslange marked this conversation as resolved.
Show resolved Hide resolved
id: string;
instanceId?: string;
label: string;
Expand Down
2 changes: 1 addition & 1 deletion assets/js/base/components/state-input/StateInputProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface StateInputProps {
country: string;
onChange: ( value: string ) => void;
required?: boolean;
errorMessage?: string;
errorMessage?: string | undefined;
}

export type StateInputWithStatesProps = StateInputProps & {
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions packages/checkout/utils/validation/getValidityMessageForInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
nielslange marked this conversation as resolved.
Show resolved Hide resolved
* 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;
61 changes: 3 additions & 58 deletions packages/checkout/utils/validation/index.ts
Original file line number Diff line number Diff line change
@@ -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';
31 changes: 31 additions & 0 deletions packages/checkout/utils/validation/isPostcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { POSTCODE_REGEXES } from 'postcode-validator/lib/cjs/postcode-regexes.js';

const getCustomRegexes = () => {
nielslange marked this conversation as resolved.
Show resolved Hide resolved
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 ) => {
nielslange marked this conversation as resolved.
Show resolved Hide resolved
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;
26 changes: 26 additions & 0 deletions packages/checkout/utils/validation/mustContain.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading