The payment page, rebuilt
The surface a customer actually pays through. Taking it from Angular 20 to 22 and dropping zone.js meant first answering an awkward question about a component library.
- Surface
- Public checkout, five routes, crypto payments on Polygon and BSC
- Work
- Framework upgrade, zoneless migration, dependency removal, pixel reconciliation
- Stack
- Angular 22, signals, native dialog, SCSS
The dependency problem
The repo sat on Angular 20.3 while its sibling was already on 22, and every piece of shared guidance had to be written twice, once per repo. Going zoneless was the other reason to move: this is a public payment surface, and after an earlier ticket removed the state library there was nothing zone-dependent left. Dropping zone.js takes a polyfill out of every bundle and a whole category of "why did this not repaint" out of the app.
One thing blocked both. The component library in use peered Angular 17 or 18 and a zone version two minors behind the installed one — both constraints already unmet before anyone proposed an upgrade. It could not go to 21, could not go to 22, and was pinned to the exact dependency being deleted.
Two ways out, and why one is not viable here
The library could be walked up rung by rung to its current major. That major introduces a licence manager: an unregistered install renders a fixed-position banner inside a closed shadow root with all: initial, on every page, with no quiet-degradation mode. The free tier exists but the key is per-repository, seat-capped and dated.
On a page where someone is entering an amount and confirming a transfer, a third party that can draw an unstyleable banner over the interface is not a dependency, it is a liability. The app used six component instances from that library, one consumer each, plus 550 of the 670 lines in the theme partial styling components this app has never rendered.
Six components is an afternoon. The licence is forever. They came out, the action sheet became a native <dialog>, and the upgrade went ahead with nothing peer-locking it.
The proof is that nothing happened
Every route was captured before and after, against a deterministic mock layer rather than production, so the same eleven scenarios produce the same pixels on demand. Across the six captured screens, four came out byte-for-byte identical to the originals. Two changed, both by design: the currency sheet, which had been the library's action sheet and is now a native dialog, and the amount card.


Pixel reconciliation and the audit
A later pass reconciled every frame against the design file at 1920, then proved the layout at 390, 768, 1024 and 1440. Divergences were fixed where the difference was ours and reported with their node ids where it was not, rather than quietly conforming to whichever side happened to be open. One rule overrode the design outright: interface titles are Title Case, because consistency inside the product beats consistency with a file that nobody using the product can see.
Accessibility ran as an audit in the same pass, with axe injected into the browser sweep rather than run once at the end and declared clean.


What the mock layer bought
The screenshots on this page contain no real customers, because the whole capture runs against fixtures: an invented merchant, an invalid-by-design domain, a phone number from the reserved range. Eleven scenarios cover the paths that are otherwise hard to reach on demand — expired, already paid, wrong chain, user rejects, transaction reverts, slow, unsupported token.
It is a build-time file replacement rather than a runtime flag, which is the part that matters: a marker string was verified absent from the production bundle and present in the mock one, and the production bundle grew by 76 bytes for the two empty arrays left behind. Test scaffolding that can be switched on in production is not scaffolding, it is a feature nobody wrote a spec for.
