Back to Feed
article 3m read

A pricing page whose paid button went to the same place as the free one

Both pricing buttons dumped people on the free dashboard after sign-in, so I started parking the plan choice in a short-lived cookie, reading it once on the way back, and sending paid picks to a checkout page that retries while the new account finishes setting up.

Article DepthPractice

Flow

The steps I built and the check I would put on each

  1. 1Button carries the plan

    Each pricing button should open the login page with the right plan in the address.

  2. 2Login page saves it

    Unknown plans should be ignored, and the cookie should leave with the redirect.

  3. 3Return to the small page

    A sign-in with no cookie should still end on the dashboard.

  4. 4Read once, delete

    A second sign-in right after a paid one should not go to checkout.

  5. 5Checkout retries, then stops

    If checkout keeps failing, the retries should end and the billing button should appear.

  6. 6Locked data, called directly

    Unknown so far: whether a free account gets refused. That is the next thing I want to try.

Both buttons on my pricing page went to the same place. The free one and the paid one both linked to the front of the app, and after sign-in everyone landed on the home page. So you could pick the paid plan, sign in, and end up on the free dashboard as if you'd never picked anything. The app never got the choice at all.

That was my mistake. All of this is from reading the change I made, which came with no tests.

Getting a choice through someone else's front door

Sign-in sends the browser to an outside login service and back. Whatever was in the address on the way out is gone when it returns. So you have to save the choice yourself before the browser leaves.

The flow now works in four steps:

  1. Each pricing button opens the app's login page with the plan in the address, including an explicit free.
  2. The login page accepts only the plans it knows and saves the choice in a short-lived cookie that page scripts can't read. It sets that cookie on the same response that sends the browser off to sign in.
  3. After sign-in, the browser comes back to a small new page instead of the home page.
  4. That page reads the cookie, deletes it every time, and sends a paid choice to checkout and everyone else to the dashboard.

The cookie doesn't need to be signed, because the only thing it does is pick a page. Faking it gets you a checkout screen, which I'm guessing you could reach from the billing page anyway.

The checkout page that has to wait for the account

A new account may not be fully set up the moment sign-in ends. So the checkout page tries to start checkout, waits a couple of seconds if that fails, and tries again a few times. Then it gives up and shows a button to the billing page.

My problem with it is that it can't tell "not ready yet" from "broken." If the price setting is missing, it still makes every new paid user wait through every retry. I want the server to say "not ready" plainly and the page to retry only then. I haven't built that.

Paid features stopped pretending not to exist

Paid areas used to be hidden behind feature switches. Now they show up in the menu with a paid badge, and free accounts get an upgrade prompt instead of the page. The billing page lost its flag too, since that's where upgrades happen.

That prompt only changes what the page shows. I still don't know if the data behind those pages checks the plan, so my next step is to call it as a free account and see what comes back.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward
Related idea

Paying customers matched by email, even the addresses the billing code made up itself

The lookup that decided whether an organization already had a payment customer searched by an email my code sometimes guessed or generated, and could hand one org's billing record to another. I moved identity onto the identity provider's org ID and tied retries to it, but the same commit left an ignored retry flag, a delete check that reads error wording, a gutted seat counter, and no tests.

via #billing · #reliability