Stripe
- 11 minutes read - 2221 wordsIt’s been almost a month since my last post. I’ve been occupied learning Stripe and integrating it into an application that I’m developing. The app benefits from a billing mechanism for prospective customers and, as far as I can tell, Stripe is the solution. I’d be interested in hearing perspectives on alternatives.
As with any platform, there’s good and bad and I’ll summarize my perspective on Stripe here. It’s been some time since I developed in JavaScript and this lack of familiarity has meant that the solution took longer than I wanted to develop. That said, before this component, I developed integration with Firebase Authentication and that required JavaScript’ing too and that was much easier (and more enjoyable).
My use-case for Stripe is straightforward. An application user will interact with Firebase Authentication, logging in using one of its supported OAuth providers, and be returned, authenticated, minimally with an email address. The email address becomes the user’s identifer for the Stripe subscription process.
I quickly found Stripe Checkout and Create subscriptions but, it’s immediately obvious that I’m self-selecting a solution. Stripe Checkout – a different link – appears to do everything I’d want. But, it begs the question: What else would Stripe provide? Am I using the correct product? I remain unsure whether I should be using Stripe Payments or Stripe Billing. I suspect that, for such a successful platform, there are a myriad products available and, since …. whatever it is I’m actually using… Stripe Checkout Subscriptions (?), fits the bill, I’m assuming it’s the only|best solution for me.
Guide
Reviewing the link to the guide Subscriptions with Checkout and the customer portal, this solution appeared to perfectly match my needs and so I plowed on.
NOTE Since I began with the guide, they’ve simplified the solution (for the better) and updated the Go module references. The guide that I encountered used a JavaScript client to programmatically call the backend (Golang) handlers. This has now been simplified to use e.g.
FORM
POST
ing which makes the guide more comprehensible. However, I think the programmatic approach is more realistic in practice. In my case, I need to make other service calls to get the e.g. user email address from the authentication service and, while these values could behidden
inFORM
’s, I think it’s probably cleaner (albeit more complex) to consider the client entirely in JavaScript. Now that the documentation has been updated (see NOTE above), I won’t labor the fact that, when I reviewed the guide, it contained minor errors and the Golang server code referenced an outdated module.
Given the above, I was slightly confused by the guide’s use of JavaScript and e.g. Golang (one of the language options and my preference). Now that this has been simplified through the use of FORM
POST
ing, this is much less confusing but, back then, the example would include, e.g.:"
return fetch("/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: body,
}).then((resp) => {
}).catch((error) => {
});
NOTE This is now replaced with a simple
FORM
POST
to/create-checkout-session
And there’d be a corresponding backend handler:
func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(
w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
return
}
...
}
Not having developed in JavaScript for some time, I found this approach confusing.
Golang SDK
The guide referenced an outdated Golang module (https://pkg.go.dev/github.com/stripe/stripe-go) and it took me a little time to realize that the latest version includes a version path (https://pkg.go.dev/github.com/stripe/stripe-go/v72). There are, of course, considerable changes between the versions and I spent some time determining the mapping from the guide’s old uses to the new.
The guide uses an (anti-pattern?) of function-local and unnamed struct types, e.g.
func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) {
...
req : = struct {
PriceID `json:"priceId"`
}
...
writeJSON(w, struct {
ErrorData string `json:"error"`
}{
ErrorData: "something",
}, err)
}
This combined, with the unstructured types in the JavaScript client made initial debugging very challenging. I suspect the Golang code may be a consequence of lack of familiarity with Golang (although writeJSON
takes an interface{}
and so the intent is to genericise the code and overcome a challenge in Golang).
In my Golang code, I refactored structs out to be package-scoped and added field descriptions:
// CreateCheckoutSessionRequest is the request type of the CreateCheckoutSession handler
// Correponds to `CreateCheckSessionRequest` class in script.js
type CreateCheckoutSessionRequest struct {
CustomerID `json:"customerId,omitempty" description:"Stripe Customer identifier"`
CustomerEmail `json:"customerEmail,omitempty" description:"Email address of Customer"`
PriceID `json:"priceId" description:"Stripe Price identifier`
}
In my JavaScript code, I attempted something similar for the fetch
body:
/**
* Corresponds to `CreateCheckoutSessionRequest` struct in stripe.go
* @param {string} priceId
* @param {string} (optional) customerId
* @param {string} customerEmail
*/
class CreateCheckoutSessionRequest {
/**
* There will always be `priceId` and `customerEmail`
* `customerId` is optional
* @param {string} priceId the Stripe Price ID
* @param {string} customerEmail the User's email address
*/
constructor(priceId, customerEmail) {
this.priceId = priceId;
this.customerEmail = customerEmail;
}
/**
*
* @returns string
*/
JSON() {
return JSON.stringify(this);
}
}
Golang testing
I’m more familiar with Golang than JavaScript and found it easier to write unit tests for the majority of the Golang code. I initially struggled wondering how to test the Golang HTTP handlers and then I discovered the Golang standard library’s httptest
… It’s excellent, e.g.:
func TestCheckoutSession(t *testing.T) {
// Used to thread the tests together
var sessionID string
// CreateCheckoutSession
{
// Create body with valid customerId and priceId
body := strings.NewReader(fmt.Sprintf(formatCreateCheckoutSessionRequest, customerID, priceID))
req, err := http.NewRequest(http.MethodPost, "/create-checkout-session", body)
if err != nil {
t.Fatal(err)
}
// ResponseRecorder
rr := httptest.NewRecorder()
// Specify the handler
handler := http.HandlerFunc(CreateCheckoutSession)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("got: %v; want: %v", status, http.StatusOK)
}
// Check the response body
// Unable to determine a priori what the `sessionId` value will be
// But we know what it should look like
got := rr.Body.Bytes()
// The structure of the JSON returned by CreateCheckoutSession
// Pattern includes a submatch `(...)` for the sessionId value
pattern := "{\"sessionId\":\"(cs_test_[a-zA-Z0-9]{58})\"}\n"
re := regexp.MustCompile(pattern)
result := re.FindSubmatch(got)
// Unable to find the submatch pattern in the response body
if result == nil {
t.Errorf("got:\n%v\nwant:\n%v", got, pattern)
}
// Returns the entire string (0) and the submatch (1)
// Want only the submatch == sessionId
if len(result) != 2 {
t.Errorf("Expected a submatch containing `sessionId`")
}
// Otherwise, result contains the sessionId
sessionID = string(result[1])
log.Info("Retrieved Session ID",
"sessionId", sessionID,
)
}
// CheckoutSession
...
// CustomerPortal
...
}
Golang deps.dev
A new and very useful Google tool that helps developers understand module dependencies and vulns. For example, here’s the output for Stripe’s Golang library:
https://deps.dev/go/github.com%2Fstripe%2Fstripe-go%2Fv72
Golang HTTP Server
For simplicity (and partly as a result of just wanting to get going), I wrote everything using a Golang HTTP server:
func main() {
ctx := context.Background()
// Use gorilla/mux for better sub-path handling (see scripts)
r := mux.NewRouter()
r.Host(domain)
r.HandleFunc("/", www.Root)
// `/success` and `/canceled` are referenced by server.go#CreateCheckoutSession
r.HandleFunc("/success", www.Success)
r.HandleFunc("/canceled", www.Canceled)
// `/tos` and `/privacy` are required by Stripe customer portal
r.HandleFunc("/tos", www.TermsOfService)
r.HandleFunc("/privacy", www.Privacy)
// Stripe server-side (Golang) handlers
r.HandleFunc("/create-checkout-session", CreateCheckoutSession)
r.HandleFunc("/checkout-session", CheckoutSession)
r.HandleFunc("/customer-portal", CustomerPortal)
r.HandleFunc("/retrieve-subscription", RetrieveSubscription)
// Stripe Webhook handler
r.HandleFunc("/webhook", Webhook)
// Serve scripts
Scripts := http.FileServer(http.Dir(dirScripts))
// See Gorilla [Static Files](https://github.com/gorilla/mux#static-files)
r.PathPrefix("/scripts/").Handler(http.StripPrefix("/scripts/", Scripts))
// Prometheus
r.Handle("/metrics", promhttp.Handler())
// Attach router to http
http.Handle("/", r)
log.Error(http.ListenAndServe(*endpoint, nil), "unable to serve")
}
I’ve removed error handling, logging and some other non-relevant functionality to convey the point. E.g. create-checkout-session
is handled by CreateCheckoutSession
and is a function that was included as part of the Stripe guide; /success
is handled by www.Success
and is a Golang templated handler that generates (templated) HTML output. My intent here was to keep it simple. In practice, of course, the Golang handlers would be self-contained and would be routed to e.g. Envoy|Caddy also handling the HTML|JavaScript.
One useful feature of gorillia/mux
that’s described on its repo’s homepage is serving static files and I’m using this to serve the JavaScripts.
JavaScript
I spent the vast (80%) of my time developing this solution trying to optimize the JavaScript. The first few iterations of my solution didn’t feel right. Eventually, I settled on a model that’s better but I still am not happy with.
Naming
As always, one challenge I had was in naming things. I’ve settled on naming:
- scripts to match pages, i.e.
root.html
(probably betterindex.html
) usesroot.js
- handlers match, i.e. JavaScript
CreateCheckoutSession
fetches/create-checkout-session
but calls Golang’sCreateCheckoutSession
handler. - structs match, as mentioned above, e.g.
CreateCheckoutSessionRequest
appears in both JavaScript and Golang - JavaScript classes match Golang types, e.g. JavaScript’s
User
class matches the Golangapi/v1alpha1/User
type.
Asynchronous hell
Ugh :-)
Whenever I return to JavaScript after an asbence, I always forget how to deal with Promises and end up with code of the form:
app.DoesSomething().then(()=>{
...
app.DoesSomethingElse().then(foo).catch(bar);
...
}).catch(bar);
And, of course, DoesSomething
’s then
rolls right over app.DoesSomethingElse
and I spend time wondering what’s going on.
My JavaScript coding rules that I began as part of this Stripe integration, now include a rule that says ‘return Promises’. By this I mean, don’t let the hang as I’ve done above but always try to:
then(()=>{
...
return app.DoesSomethingElse().then(foo).catch(bar);
});
Of course, this then means, that the Promise returning function must be the last statement in the block but, generally, this is what it should be; calling DoesSomethingElse
results in some follow-on asynchronous behavior that necessarily should affect my code’s flow.
In Golang, it’s conventional (and I think the linters complain) at using redundant (!) else
blocks if the then
block returns, i.e.:
if condition {
...
return ...
} else {
...
return ...
}
Is better written:
if condition {
...
return ...
}
...
return ...
}
This is easier to read, keeps life simpler if there are many if
blocks, and ends functions|methods with a (otherwise) return
statement.
I’m unclear whether this is considered a best practice in JavaScript. I felt compelled to add else
statements particularly with the prevalence of catch
callbacks but, I dislike the complexity that results:
app.DoesSomething().then(()=>{
if (condition) then {
...
} else {
...
}
}).catch(()=>{
if (condition) then {
...
} else {
...
}
});
Debugging
I really enjoy debugging JavaScript and (Chrome’s) DevTools are excellent. My only gripe is that, with my frequent code changes and page refreshes, breakpoints stick to line numbers rather than code and so I was forever moving breakpoints to stick to e.g. the first statement after then
and catch
blocks:
app.DoesSomething().then((data)=>{
... // <== I want a sticky breakpoint here
}).catch((error)=>{
... // <== I want a sticky breakpoint here
});
Testing
Unlike with Golang, the JavaScript testing is mostly non-existent and this gives me pause. Because it depends on the Golang backend handlers, the Golang server will need to be available for the JavaScript tests.
Interestingly, considering how to unit test, was what helped me identify how better to structure the code. As described above, I renamed scripts to reflect the hosting HTML page (i.e. root.js
in root.html
) and then I realized that root.js
should not contain CreateCheckoutSession
since this functionality was app (not page) functionality. This got me to the point where I could only test the JavaScript CreateCheckoutSession
function. Assuming I had a server though, I’d still want to establish Stripe Price, Customer resources.
This is where I’ve currently given up but I know I need to return here.
Stripe CLI (Excellent)
I think formative years using Windows and fighting anxiety whenever I installed apps and being uncertain how much pollution was being added to the registry and wondering where all the files were that the app had created, means that, even today using Linux (which is much cleaner and clearer), I’m still reluctant to install apps onto my regular file system. I says this because, I do use Snaps when I can (though I’m still uncertain how to prove their provenance) and generally prefer to use Docker when I can.
Fortunately, Stripe’s excellent CLI is packaged as a container image (stripe/stripe-cli
). What was less clear is how to manage its configuration. I’ve settled on and continue to try to be more diligent about never using :latest
(or any other tags really), hence the DIGEST
:
DIGEST="sha256:2c89715f325b1ec80b5742c50f0bb6f6844e49da73aa7d87effe4a992f09927f" # v1.6.4
docker run \
--interactive --tty --rm \
--volume=${HOME}/.config/stripe/config.toml:/root/.config/stripe/config.toml \
stripe/stripe-cli@${DIGEST}
One small trick is to use the flag --color=off
in order to get it to generate valid JSON output that can be filtered through e.g. jq
# By default stripe's CLI does not output valid JSON
stripe products list \
| jq .data[0].id
parse error: Invalid numeric literal at line 2, column 4
# Using the flag `--color=off` results in valid JSON
stripe products list --color=off \
| jq .data[0].id
"prod_..."
Stripe Dashboard and Test data (Excellent)
Stripe’s developer testdashboard is really excellent, fully functional and mostly intuitive.
I really like that Stripe provides production (live) and test data. What an excellent idea! I’m totally going to try to steal this approach and use it within my application.
Stripe provides 2 sets of developer credentials (test and production).
The dashboard includes a switch (“Viewing test data”) and, when you’re viewing test data, a possibly too innoucuous message TEST DATA
hovers over the dashboard. I’ve submitted feedback suggesting it be configurable to make the dashboard background dangerous red for production and calming green for test data!