« Beeminder home

Beeminder Blog

Beeminder hearts Stripe

Circa 2019 or so Stripe released a big update to their Checkout product, the previous iteration of which we’ve been using to collect your payment info on Beeminder for over 11 years now. This is the tale of how it took us four years to migrate to that new version.

Alright, it did not literally take four bee-years of coding time to complete this. Much of it was just other priorities crowding out time spent on it. But over the past year, as more and more of our users were impacted by the European Strong Customer Authentication regulation, it gradually became urgent for us to get up to date and support 3D Secure payments.

Our hoary codebase had also accumulated technical debt over the last decade or so that meant we had several prerequisite updates to make before we could actually get started on updating the version of Stripe Checkout itself.

1. Technical Prerequisite: update how we access your stored card info

At some point Stripe changed the API for what they call a stored payment method, and switched from calling it a “source” to calling it a “payment method”. So we had to start with a simple refactor from using their older moniker to the newer one. This was mostly a search-and-replace; we just also needed to update all of our existing customers on Stripe to copy their stored “sources” over to “payment methods” as well.

This was totally behind the scenes and made no user-visible differences.

2. Another Technical Prerequisite: update how we draw money from a stored payment method

The next step was to update our code away from using Stripe’s Charges API, to using their newer Payment Intent API. The legacy Charge API doesn’t support customers in India, bank requests for authentication, or secure customer authentication. Meaning that many of the cards we collect through the new Checkout would be unusable with the older Charges API that we were using, but the Payment Intents are backward-compatible and work fine with the old stored cards.

2a. Sidetrack from the Prereq: update how we charge for derails

As part of migrating to Payment Intents, we needed to refactor how we charge for derails. This was another user-invisible refactoring project.

Charging for derails is the oldest piece of payment infrastructure in the Beeminder codebase, and it was done slightly differently from how we create other charges (like premium payments, API-created charges, and various failed experiments like paying for freebees, or running contests.)

Anyhow, we had two different ways we were creating charges, and before we switched over to Stripe’s Payment Intents, I spent a week isolated in an AirBnB, maniac-style, refactoring the payments code so that we only had one way that we create charges.

(This is of course ignoring the pox on our codebase that is PayPal payments… uff-dah. More on this at the end of the post.)

2b. Detour complete: switch to using Payment Intents

Finally, I switched things over to using Payment Intents. It’s still a bit dodgy what with the PayPal Pox and I can’t wait to heal that up with some modern medicine. I hear that you can set up PayPal as a payment method in Google Pay, but maybe only in the US?

3. Stripe Payment Elements

Then finally! I was ready to upgrade Stripe Checkout. But I was bothered that the new version of Checkout involves a redirect to a Stripe hosted page, instead of just being an update to the javascripty popup that we’d had from the old version of Checkout.

For comparison, here’s the old version: Popup window with credit card fields

And the new version: Other popup window with credit card fields

That redirect maybe makes more sense when what you’re talking about is a shopping cart experience, but it felt pretty weird to me in our context, where you’re just storing a card with us to be charged at some later date. Stranger still when the redirect is part of the process of signing up for a new account. Because yes, we collect a card up front when you sign up for Beeminder.

But lo! Stripe has an alternative to Checkout; they have their Elements API, which is a handy Javascript library with a bunch of prebuilt UI components for collecting payments (or setting up a payment method for future usage, as in our case). You essentially use some Javascript to drop in a whole ready-built widget, and another call to the Javascript library to submit it back to Stripe for packaging.

Back in 2019 when this saga started, Elements had fewer bells and whistles, but in the intervening four years of not getting around to updating our Stripe code, Stripe had been adding to Elements. Now it’s got a single “Payment Element” you can mount into the page with a line or two of Javascript, and then Stripe dynamically figures out, based on the client’s locale, what types of payment options to offer them, and in what order. And new payment method types can be added with just a line of configuration (just like Checkout).

But… turns out that using a Payment Element to set up future payments still requires a redirect. That’s part of 3D Secure that you can’t guarantee a way out of. Sometimes a payment method or a specific bank won’t require it, but you’ve got to handle it when it is required.

Which means I find myself tumbling down a new rabbit hole (within a rabbit hole). The amount of work required to use Payment Elements in our registration form is starting to balloon. I’m starting to think about handling that redirect, and wondering how to store the user’s registration info that they have dutifully entered into the form already. I’ve got browser local storage, and cookies, and URL parameters, and Stripe metadata dancing through my head. I’m wondering if I need to design a new API endpoint for creating users. This is starting to look more complex than it did at first blush.

4. Get practical, and decide to use the expedient Checkout

And suddenly Checkout’s redirect doesn’t look so bad. The redirect that I’m not getting out of anyway.

In fact, Danny even suggests that perhaps the redirect is better than having a credit card form that looks like a seamless part of our signup form? Perhaps Stripe knows best. Perhaps users will think that we’re the ones storing their payment info if it’s part of the same form. You never know.

(PS: Support Czar Nicky confirms that users totally have commonly worried that this was the case.)

Using Checkout I get to:

  1. Avoid writing new Javascript code
  2. Use the existing code for handling user signup
  3. Actually remove some error-handling code from the signup page
  4. Not worry about changes to, e.g., “Sign up with Google” (because, see above: I’m using the existing code for handling signup)

So far I’ve mostly only talked about Checkout in the context of the signup page. For users who were grandfathered in before we required adding a payment method as part of signing up, there are also commitwalls (i.e., we ask for you to store a payment method) before you can sign up for premium, and as part of starting a goal. We also allow you to change the payment method attached to your account from the payments page at any time. New Checkout works with those scenarios as well.

There was very little Javascript code for me to write. Mostly just un-writing. And this part only took about 9 days to do. I made the first commit on my “upgrade checkout with redirect style” on January 31st.


PayPal vs… everything else

Now that we’re using modern Stripe APIs, the next obvious step is more payment methods! We’re planning a second, non-nerd blog post about all the new payment methods we’re now able to support. It includes Google Pay and Apple Pay so far. Hopefully it will be a big enough spoonful of sugar for any holdouts who may be sad about us dropping PayPal.

But we’re pretty dead set on dropping PayPal. We added support for it in 2017 and have been regretting it ever since. You might think if there are users who want to give us money and that’s where they have their money then we should, y’know, take it. But it’s hard to overstate how much of a pain it’s been. It’s not even a problem specific to PayPal necessarily, though they do make everything annoying compared to the joy that is Stripe. It’s more that having an extra way to collect money from users makes every block of code that touches payments that much more complex. If-statements everywhere! And you know how we hate those. Basically, supporting PayPal throughout this whole upgrade saga was another thick layer of tech debt molasses slowing everything down and it’s going to feel amazing to finally jettison it.

We do want to do it as gently as possible, transitioning the PayPal users over to other payment methods rather than literally dropping support and losing those users. But we will eventually pull the bandaid off.