Skip to content

Form validation

Imogen Hardy edited this page Nov 22, 2022 · 6 revisions

This page pertains to the form validation system in place for the supporter plus product checkout released in November 2022.

But why?

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.

What do we need to validate?

The core things we need to validate are:

  • Personal details, including an email address that looks reasonably valid
  • String 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.

How do we validate?

There are three key pieces of the form validation system on the checkout:

Schemas

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.

The validateForm action

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.

Error selectors

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.

What happens after validation?

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.

When do we validate?

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 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.

πŸ™‹β€β™€οΈ General Information

🎨 Client-side 101

βš›οΈ React+Redux

πŸ’° Payment methods

πŸŽ› Deployment & Testing

πŸ“Š AB Testing

🚧 Helper Components

πŸ“š Other Reference

1️⃣ Quickstarts

πŸ›€οΈ Tracking

Clone this wiki locally