Merx

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.

/site/blueprints/fields/personal-data.yml
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.

/site/templates/checkout.php
…
<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.

POST https://shop.test/api/shop/checkout
{
    "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.

/assets/js/checkout.js
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

/assets/js/checkout.js
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.

  1. Customer enters personal data, selects payment gateway and submits the checkout form.
  2. 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)
  3. 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.
  4. Customer is redirected back to the shop. (/api/shop/success)
  5. Order page is created with data from the session
  6. Optional: Order is marked as complete via hook (asynchronous payment).

Next, learn how to show the order information to the customer.