Dansday

The Month the Popup Had No Close Button

Published on Apr 30, 2025

Fourteen pull requests in April, twelve merged, three closed. Thirty-five contributions. The month contains the most embarrassing thing I found in eighteen months of this codebase, and a decision I am fairly proud of, and they are both about knowing when to stop.

No way to dismiss it

In January I wrote about CatCare, the device insurance we started selling at checkout, and about the five rounds of design review it took — UI 2, UI 3, UI 4, UI 5, each one its own pull request, two of them needing a same-day fix.

On 15 April I merged this ticket:

Final CatCare Popup: Add a close button

For the final catcare popup, add an HTML close button (similar to exit
intent popup). Currently, users have no way to dismiss this, besides
refreshing the page.

The insurance confirmation popup had been live since January with no way to close it. Five rounds of design review, four separate pull requests of pixel adjustments, screenshots at three breakpoints, and nobody — not the designer, not the reviewers, not me — had asked what happens if the customer does not want it.

I have thought about why, and I do not think it was carelessness so much as the direction everyone was looking. Every one of those five rounds was about whether the popup was persuasive: the badge, the title, the illustration, the spacing. The question how does someone leave is a different kind of question, and none of the reviews were shaped to ask it. A design review that only evaluates the intended path will approve a room with no door.

The fix is 169 additions and 141 deletions, and the ticket itself tells you where the pattern came from — similar to exit intent popup. The popup we built to catch people leaving was the one that knew how to let them go.

Two hotfixes and a revert

On 8 April a colleague opened a substantial operations feature: automatically capturing the order type and fulfilment store on every order, so the team could tell a delivery from a paid reservation from a free reservation and know which shop was fulfilling it. 533 additions, 216 deletions, thirty-one files. It merged on 14 April.

On 21 April I opened a pull request called Hotffix pickup voucher, typo included — 205 additions, 185 deletions, fifteen files. On 22 April I opened Hotffix pickup voucher 2. And also on 22 April, I opened a third pull request that reverted the colleague's feature entirely, and merged it the same day. The next day I closed both of my hotfixes.

The pull request bodies are empty, so I cannot read the reasoning back out of the repository. What I can read is the overlap. The feature and my first hotfix touch ten of the same files, and they are the ones that matter: the cart controller, the frontend order controller, the delivery options template, the checkout page, and the pickup shipping carrier along with its configuration and its language file.

Which points at something I flagged myself in October, when I built the delivery-conditional vouchers. Those vouchers decide whether they apply by inspecting the cart's shipping method:

$cartShippingCode = explode('_', $cartShippingMethod)[0];

I wrote at the time that this was a string operation standing in for knowing the format. Six months later a feature that reorganised how orders classify their shipping and fulfilment went in, and vouchers on pickup orders broke.

Two attempts at patching over it did not converge, and the third option was to put the shop back the way it was and let the feature be rebuilt with the voucher interaction understood. That is the right call and it is not a comfortable one — it means deleting 533 lines of somebody else's merged, reviewed, working-in-staging code, eight days after they shipped it. I would rather do that than leave a checkout where a voucher silently fails on one fulfilment path.

The thing I would do differently is write down why, in the pull request, at the time. A revert with an empty body and the text Reverts #1322 is a decision with no reasoning attached, and the person who rebuilds that feature deserves to know what broke rather than having to diff their way to it. I have complained about empty issue bodies twice in this series. This one was mine.

Exit intent

The largest feature of the spring landed on 8 April: 1,204 additions, 825 deletions, forty files. It watches for a customer about to leave — the mouse travelling toward the browser chrome, the tab losing focus — and offers them a reason to stay.

Two follow-ups tell you what it actually cost. A staging fix titled Optimize more blades for whatsapp number touching twelve templates, and three days later an update to what the popup pre-fills into the chat message. By this point the WhatsApp handoff existed on the product page, the checkout, the store cards, the out-of-stock state, the exit-intent popup and the instalment panel, each with its own prefilled message, and adding one more meant visiting all of them.

A timer that follows you

Late in the month I built a cumulative timer for the product page and the checkout popup. The ticket explains itself unusually well:

User behaviour we observe:
1) Customers navigate between multiple PDPs to compare between different products.
2) Customers spend time exploring checkout page, figuring out what/how
   everything works.

We want to maximise the # of users to start a conversation with us. When
users do not have their needs met on PDPs or Checkout page, they might
leave to explore/purchase elsewhere.

The word doing the work is cumulative. It is not a timer on a page — it accumulates across pages, so a customer who spends forty seconds on three different phones has spent two minutes deciding, and that is the signal. Somebody comparing devices for two minutes without asking anything is either close to buying or about to leave, and both are worth interrupting.

It is good instrumentation and it belongs in the same paragraph as three other things I have written up: the Low Stock badge that says almost gone at zero quantity, the Rare Gem! label for products nobody buys, and now a clock that watches how long you hesitate and prompts you when you do. None of them is dishonest. All of them are the shop getting better at reading and answering a customer's uncertainty, which is what a shop is for. I only note it because the first article in this series was about making a page stop overstating things, and eighteen months of tickets have been steadily teaching the same page how to persuade.

Blocking WhatsApp

My favourite three lines of the month:

User-agent: WhatsApp
Disallow: /

Every WhatsApp call-to-action on the site produces a link, and every time somebody shares one, WhatsApp fetches the destination to build a preview card. On a site whose entire support model runs through WhatsApp, that is a crawler with an unusual amount of enthusiasm for your product pages. Three lines of robots.txt, and it stops asking.

It is a reminder that a site's traffic is not only customers. It is search engines, preview bots, monitoring, and whatever else has found a reason to fetch a page, and any of them can become the load you are optimising against.

The logo came off a month late

On 19 February I swapped the company logo for a Ramadan version, using the same two-file trick as December's Christmas hat — overwrite the image, keep the original beside it under a new name.

On 30 April I merged 1347: Revert to original logo.

Ramadan ended at the end of March. The festive logo was up for about a month after the festival it was celebrating, because taking it down was nobody's ticket until somebody noticed. December's hat came off on New Year's Eve, exactly on time, which I suspect is because the end of December is a date everybody has in their head.

This is the smallest possible argument for automation and I still do not think it justifies building a seasonal logo system. It is a good argument for creating the removal ticket on the same day as the addition.

A slide-out with the return policy in its filenames

370 additions, 204 deletions, fourteen files, and absent from this article as written. Deflate it for the regenerated sprite manifest — 295 of those additions — and the real work is a new 49-line component, header-sidebar.blade.php, plus nine lines of CSS and six new icons.

It is a slide-out panel from the header carrying the shop's six promises. The icons are named after them:

sidebar-1-year.png
sidebar-10-day.png
sidebar-battery-health.png
sidebar-exchange.png
sidebar-savings.png
sidebar-test.png

The second one is the return window. In April 2025 the ten-day return policy became part of an image filename, and in December 2025 the policy changed to fifteen days — 260 additions and 296 deletions across eleven files, because the number was written into copy in three places rather than read from one. I wrote that up in December as a lesson about hardcoded figures. This is where one of the copies came from, and it is worse than a string in a language file: a filename cannot be found by searching for the word it encodes.

The same pull request also removed three lines from config/header.php, which is the old approach coming out as the new one goes in — the useful half of a redesign that the line count hides.

Scrolling to the error, six hundred and seventy-five pixels up

Sixty-seven additions, six deletions, three files: Always insert Chat CTA when there is a payment issue. It is on the chat-button thread this article already discusses, and I left it out.

The behaviour is right. If the checkout comes back with a payment error, the page should take the customer to it rather than leaving them looking at an unchanged form. The implementation is this:

scrollToPaymentMethod() {
    const paymentError = this.paymentErrorTarget;
    if (paymentError) {
        const rect = paymentError.getBoundingClientRect();
        const absoluteElementTop = rect.top + window.pageYOffset;
        const scrollPosition = absoluteElementTop - 675;

        window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
    }
}

Six hundred and seventy-five pixels. Not a variable, not a computed header height, not scrollIntoView with a block option — a number that was correct on the viewport I was testing on. Anything that changes the height of what sits above the payment section moves the target, and nothing in the code will say so. It is the same class of thing as the floating chat button colliding with whatever happens to be above it, which I have now written about in January, and will again in May and June.

Also in the month and unmentioned: the personal-loan payment assets were renamed from cash-loan to personal-loan and re-exported, and the New Store labels got another pass in config — the third month running that badge was edited, and one month before it was removed entirely.

What April was

Fourteen pull requests. An exit-intent popup live, a close button on a popup that had needed one for three months, an operations feature reverted to protect the checkout, and a crawler politely asked to stop.

The two I would put next to each other are the close button and the revert. One is a thing I failed to notice for three months because every review was pointed at whether the popup worked, not whether it could be escaped. The other is noticing within eight days that something had broken and being willing to delete a colleague's shipped work to stop it. The difference between those two is not skill. It is that the checkout has customers hitting it every hour and the popup only had reviewers looking at it, and the reviewers were all looking in the same direction.

Revised: this article originally omitted the header slide-out, the payment-error scroll fix and the loan-asset rename — about a quarter of April's changed lines. The sections above were added from the original diffs.