-
Notifications
You must be signed in to change notification settings - Fork 13
Form validation
This page pertains to the form validation system in place for the supporter plus product checkout released in November 2022.
We need to perform as much validation as possible client-side in order to prevent failures in the back end, whether on the support-frontend server or in the step functions. These failures can be frustrating and confusing to the user, and cause us to lose revenue. It's much better to enable the user to correct any bad data client-side before they submit the form, to make the resulting transaction as quick and easy as possible.
The core things we need to validate are:
- Required personal details are present, including an email address that looks reasonably valid
- Text inputs do not contain emojis
- Any custom amount entered is a valid numeric amount between the maximum and minimum
- Required address details are present
- A payment method has been selected
- Any specific details required for that payment method are correct
We need to validate this information regardless of whether it's been entered by the user or provided by an integration, eg. Apple/Google Pay.
There are three key pieces of the form validation system on the checkout:
Any Redux slice that stores user input should have a schema against which that input can be validated. Currently we use zod to create schemas and inferred types from those schemas. The schema defines the rules for that input in a single place that's easy to understand and amend, and easy to use to check data validity. Schemas should cover as much of the potential user input as possible, even if some of it won't be entered in certain circumstances- for example, the user details schema has rules for first name and last name, despite these fields not being requested when making a one-off contribution.
See the SEPA schema for an example.
Most actions in our Redux store are tied to a specific slice, and the action creator functions are created automatically by Redux Toolkit based on that slice's reducers. However Redux Toolkit also enables us to define action creators independent of specific slices, and validateForm
is such an action creator. It should be dispatched, with an optional PaymentMethod
payload, when you want to perform validation across the whole form.
Every slice that needs to perform validation should listen for validateForm
in its extraReducers
, and perform whatever validation is required for that specific slice in order to display any errors to the user. In most cases this will involve attempting to parse the slice state against its associated schema, but it might require parsing only one field, or doing something else entirely.
Selectors are functions which accept the Redux state as an argument and return some information derived from that state. When performing form validation, selectors enable us to translate between the very general-purpose schemas- and the errors they generate- and the specific needs of the actual checkout the user is completing. So, for example, after dispatching validateForm
on a one-off contribution checkout the schema will have generated errors for a missing first name, last name, email confirmation, etc, but we know that for that specific product we don't need them and so can use the selector to choose to display only the errors that are relevant.
Errors should be passed through to the form components they relate to- for example, errors for the first name, last name and email address should be passed to the PersonalDetails
component- in order to be displayed inline next to the relevant field. See an example of this in Source.
We also display an error summary component at the top of the form which receives automatic focus on render. This lists all errors in the form as anchor links to the relevant field, making it easy for users to quickly go to the place where they've made an error and amend it.
The majority of the time we validate just before the user tries to pay. The useFormValidation
hook is intended to make this easy for the majority of use cases- it accepts any payment-specific form submission function, and returns a function that will run form validation and then either block submission if there are errors, or proceed and run the passed function if form validation succeeds.
There are a couple of special cases where we perform validation at other steps in the form:
- For recurring PayPal payments, we validate when the user selects this payment method and the button renders. This is because there are no other steps between this and the user clicking the button to pay, so it prompts them to fix any errors with their entered details before opening the PayPal interface.
- For the Stripe card form, because the input fields are actually iframes to a Stripe domain we have to rely on information from the components' change events to determine if there are errors relating to any field. Thus while we only display errors at the point of attempted payment, error messages may be generated and stored in Redux as the user fills out the form.
- For Apple/Google Pay, we validate only the 'other amount' field before the user clicks the button to bring up the payment interface, and after they complete the payment we run validation against the personal details and address information we receive from the payment provider via Stripe. Errors with this information- for example, a user having an emoji in the first name they used to sign up for Google Pay- are not something we can meaningfully recover from or ask the user to correct, so if validation of this information fails we simply show a message asking the user to try another payment method.
- Redux Glossary
- Why Redux Toolkit?
- Writing state slices with Redux Toolkit
- Handling action side effects in Redux
- Presentational and Container Components
- Scoped actions and reducers
- Server Side Rendering
- Form validation
- CI build process
- Post deployment testing
- Post deployment test runbook
- TIP Real User Testing
- Code testing and validation
- Visual testing
- Testing Apple Pay locally
- Test Users
- Deploying to CODE
- Automated IT tests
- Deploying Fastly VCL Snippets
- Archived Components
- Authentication
- Switchboard
- How to make a fake contribution
- The epic and banner
- Environments
- Tech stack
- Supported browsers
- Contributions Internationalisation
- Payment method internationalisation in Guardian Weekly
- Print fulfilment/delivery
- Updating the acquisitions model
- Runscope testing
- Scala Steward for dependency management
- Alarm Investigations
- Ticker data
- Ophan
- Quantum Metric
- [Google Tag Manager] (https://github.com/guardian/support-frontend/wiki/Google-Tag-Manager)