Validate checkout
Learn how to validate customer’s data.
To make sure you get all the data you need from your customers, Merx uses Kirby’s page validation. You can define the required fields and set validate options for each field you want to validate. See Kirby’s documentation for all available validators. If you need more specific validation, you can create your own validators.
Define fields
Set up the fields that are required to fulfill orders as a field group of your own. If you don’t create one, the default personal data fields are used, which only ask for an email address.
type: group
fields:
email:
label: Email
type: email
required: true
given_name:
label: Given name
type: text
required: true
validate:
minLength: 2
family_name:
label: Family name
type: text
required: true
organization: # Optional field
label: Organization
type: text
# …
Update checkout template
Add the new fields to the checkout template.
…
<form id="checkout-form" action="<?= url('api/shop/checkout') ?>" method="post">
<label>
Email
<input type="email" name="email" autocomplete="email" required>
</label>
<label>
Given name
<input type="text" name="given_name" autocomplete="given-name" required>
</label>
<label>
Family name
<input type="text" name="family_name" autocomplete="family-name" required>
</label>
<label>
Organization (optional)
<input type="text" name="organization" autocomplete="organization">
</label>
<input type="hidden" name="payment_gateway" value="invoice">
<button>Submit order</button>
</form>
…
To build these inputs from the blueprint instead of writing them out, see Build the checkout form from the blueprint.
Example response
When there are no validation errors, the API endpoint returns a redirect response, which redirects to the payment provider or directly to the order page.
When the validation fails, the API returns a JSON with one or more error messages per invalid field.
{
"status": "error",
"message": "Field validation failed.",
"code": 400,
"key": "error.merx.fieldsvalidation",
"details": {
"email": {
"label": "Email",
"message": {
"required": "Please enter something"
}
},
"given_name": {
"label": "Given name",
"message": {
"required": "Please enter something",
"minlength": "Please enter a longer value. (min. 2 characters)"
}
},
"family_name": {
"label": "Family name",
"message": {
"required": "Please enter something"
}
}
}
}
Use JavaScript to handle form submit
As in Adding products, we intercept the submit event of the checkout form, and use the validation details to show the error messages to the customer.
const checkoutForm = document.getElementById('checkout-form');
checkoutForm.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch(checkoutForm.action, {
method: 'post',
body: new FormData(checkoutForm),
headers: {
'Accept': 'application/json',
}
});
console.log(response);
/** @see https://merx.wagnerwagner.de/reference/api/routes/shop-checkout-post */
const json = await response.json();
if (json.status === 'redirect') {
// [1] Redirect user to payment provider or directly to order page
window.location.href = json.redirectUrl;
} else {
// [2] Handle validation errors
const errorDetails = json.details;
console.log(errorDetails);
}
});
Complete validation handling
const checkoutForm = document.getElementById('checkout-form');
/**
* Creates a p element with class “error”.
* @param {String} error e.g. “Please enter a valid email address”
*/
function createErrorElement(errorMessage) {
return Object.assign(document.createElement('p'), {
classList: 'error',
innerText: errorMessage,
});
}
/**
* Removes all elements with class “error”.
*/
function clearErrors() {
checkoutForm.querySelectorAll('.error').forEach((_) => _.remove());
}
checkoutForm.addEventListener('submit', async (event) => {
event.preventDefault();
clearErrors();
const formData = new FormData(checkoutForm);
// Disables all form fields to prevent multiple submissions
[...checkoutForm.elements].forEach(_ => _.toggleAttribute('disabled', true));
checkoutForm.setAttribute('data-state', 'loading');
// Send form data to the API endpoint
const response = await fetch(checkoutForm.action, {
method: 'post',
body: formData,
});
/** @see https://merx.wagnerwagner.de/reference/api/routes/shop-checkout-post */
const json = await response.json();
console.log(json);
// Enables all form fields
[...checkoutForm.elements].forEach(_ => _.toggleAttribute('disabled', false));
checkoutForm.removeAttribute('data-state');
if (json.status === 'redirect') {
// [1] Redirect user to payment provider or directly to order page
window.location.href = json.redirectUrl;
} else {
// [2] Handle validation errors
try {
if (json.key === 'error.merx.fieldsvalidation') {
// Handle field errors
const errorDetails = json.details;
for (const [key, error] of Object.entries(errorDetails)) {
const labelElement = checkoutForm.querySelector(`label:has([name="${key}"])`);
if (labelElement) {
for (const errorMessage of Object.values(error.message)) {
// Display error message(s) inside of label element
labelElement.appendChild(createErrorElement(errorMessage));
}
} else {
throw json.message ?? 'Fatal error';
}
}
} else {
throw json.message ?? 'Fatal error';
}
} catch (errorMessage) {
// Display general error message before submit button
checkoutForm.insertBefore(createErrorElement(errorMessage), event.submitter)
}
}
});
You should add some CSS styles for #checkout-form[data-state="loading"], so that the customer knows that something is happening.
Checkout flow
We’re using invoice as the payment gateway here, which means it’s up to you as the shop operator to collect the money.
In practice you might use a payment provider such as Stripe or PayPal to collect money from your customer during the checkout.
Depending on the payment provider and the payment gateway the checkout process might vary.
For some payment gateways the payment is handled asynchronously. This means that the money is received by the payment provider after hours or days. As soon as the money has been received, the payment provider notifies the Merx shop via hook.
- Customer enters personal data, selects payment gateway and submits the checkout form.
- Personal data is validated. When valid, the data is stored together with the cart in the customer’s session on the server. (
/api/shop/checkout) - Optional: Payment details are validated and payment process is handled on page (e.g. credit card via Stripe) or customer is redirected to payment provider (e.g. PayPal) to handle the payment process.
- Customer is redirected back to the shop. (
/api/shop/success) - Order page is created with data from the session
- Optional: Order is marked as complete via hook (asynchronous payment).
Next, learn how to show the order information to the customer.