lukia lab

React island · @iamlukia/za-id

Merchant onboarding

Four steps, real South African validation, and the two things a form demo normally leaves out: an error summary that works when you cannot see the fields, and a save that fails on purpose so the recovery path is something you can look at rather than something I claim exists.

This is the unglamorous piece. It is also the one closest to what client work actually is — most of a build is a form, and most forms are wrong in the same four ways.

The application

  1. 1Business
  2. 2Director
  3. 3Banking
  4. 4Review

Business, step 1 of 4

The entity that will hold the merchant account, as it is registered.

Exactly as it appears on the CIPC registration certificate.

What customers see on their statement, if it differs from the registered name.

CIPC format — four digits, six digits, two digits. For example 2019/123456/07.

Ten digits beginning with 4. Leave it blank if the business is not VAT registered.

Nothing here leaves the page — the site sends connect-src 'none' and form-action 'none', so it could not if it tried. Two failures are deliberate and reachable: a branch code of 000000 is rejected by the bank, and a registered name containing timeout never gets a reply. The banking step also fails its first background save, on purpose.

How it is built

The SA ID number is checked by @iamlukia/za-id, the package behind the validator on the main site — imported here rather than copied, which is worth a sentence because the main site does the opposite. That site ships no JavaScript at all, so its copy of this logic is inlined uncompiled and kept honest by a parity script. This page ships bundled React on purpose, so it can just use the package.

It reports why a number failed rather than only that it did, and the five reasons are mapped to copy written for somebody who has mistyped one digit of their own ID number — not for a developer reading a result object.

Nothing is judged until you have left it. Validating on every keystroke means announcing “Email address is required” to a screen reader user who has typed one letter. Fields are checked on blur, and everything on a step is checked when you press Next.

Failing a step gives you an error summary, and focus goes to the summary rather than to the first bad field. Focusing the field is the obvious move and it tells you about exactly one problem — you fix it, press Next, and get thrown at the second. Each line in the summary moves focus to the field it names.

Saves are optimistic and one of them fails. Advancing marks the step saved immediately, before the request that has not happened yet. The banking step fails its first attempt every single time, deliberately: a rollback behind a random number cannot be demonstrated on request, cannot be tested, and looks like a bug when it fires. You are not blocked by it — you can carry on and retry.

Two more failures are reachable on demand. A branch code of 000000 is rejected by the bank at submit; a registered name containing the word timeout never gets a reply. Both are pure functions of what you typed, so both are covered by tests.

Reduced motion removes the step transition rather than shortening it — and then puts back what it was doing. A transition is not decoration here: it is what tells you the panel in front of you is a different panel and that the last one was accepted. With the animation gone, a line of text says so instead. That decision is a pure function with tests over it rather than a media query somebody has to remember.

Nothing leaves the page, and it could not. The site sends connect-src 'none' and form-action 'none', so there is no network call to inspect and no submission to intercept. Those two directives were written before this piece existed, as tripwires for it; the decision it had to take was whether to widen them, and the answer was no.