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
For comparison, here’s the old version:
And the new version:
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… 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:
- Use the existing code for handling user signup
- Actually remove some error-handling code from the signup page
- 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.
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.