Avatar of Matt Moriarity
Matt Moriarity

Using Stripe Checkout in an Elm 0.19 App

I’m currently working on a web project whose frontend is written in Elm. The project uses Stripe for handling payments. I ❤️ Stripe: it makes billing much easier for me and lets me mostly focus on the more unique aspects of the project.

For now, I’m using Stripe Checkout, the simplest way to get payment details into Stripe. With Stripe Checkout, Stripe’s JavaScript creates the form for getting payment details from the user, and it shows that form in a modal window over your page. When the user submits the form, it talks to Stripe, gives you a token representing the payment details, and sends that to an endpoint on your server.

Integrating Stripe Checkout in Elm 0.18

The simplest way to use Stripe Checkout is to make an HTML form and put a script tag inside it, with data attributes to customize the payment form. This is what I was doing with my app when I was using Elm 0.18:

stripeButton : Model -> Html Message
stripeButton model =
Html.form
[ action "/subscribe"
, method "POST"
]
[ node "script"
[ src "https://checkout.stripe.com/checkout.js"
, class "stripe-button"
, attribute "data-key" model.stripeKey
, attribute "data-name" "My Product"
, attribute "data-description" "Description of my product"
, attribute "data-amount" "500"
, attribute "data-zip-code" "true"
]
[]
, button [ type_ "submit", class "button is-link is-medium" ]
[ icon Solid "credit-card"
, span [] [ text "Subscribe for $5/mo" ]
]
]

Using a custom HTML node to include a script tag in an Elm app is admittedly pretty dirty. But it’s relatively contained and it was working fine. The button there is actually a bit of a hack. Stripe Checkout will render its own button in the form, but it doesn’t look great with the rest of my UI, so there’s CSS to hide the Stripe button. As long as this button submits the same form, it’s just as good, and I can customize it to look how I want.

Elm 0.19 ruins the fun

Elm 0.19 is pretty disruptive: it changes a lot about how the language and many of its core libraries work. I went through the pretty gnarly upgrade process and thought I was in the clear. After all, one of the nice things about Elm is that once you get it compiling, you’ve usually caught most of your problems.

In this one case, though, the upgrade had introduced a silent failure: my payment button didn’t work anymore! Instead of opening Stripe’s payment form, clicking the button just submitted the form (without any payment info), which then gave an error. Not what I wanted at all.

I’ll spare you the hours of diagnosis and just tell you what the problem was: Elm 0.19 is fundamentally incapable of putting a script tag in HTML. A script node will get silently converted to a p node to prevent cross-site scripting attacks at the language level. Since the virtual-dom is generated with a Native module, and in Elm 0.19 third-party Native modules are no longer supported, there’s no way around this limitation. It’s a deliberate design decision. Even if we could work around it, we’d be fighting the future of the language, so instead let’s figure out a way to use Stripe Checkout without fighting Elm.

Ports to the rescue!

Elm does have a way to work with normal JavaScript code, and it’s pretty clever. It’s called “ports,” and it’s clever because it provides a way to send messages to and from JavaScript without breaking the functional nature and safety of Elm. Incoming messages from JavaScript become Messages which you handle in your update function, just like the ones your app creates to handle DOM events and HTTP responses. Outgoing messages are sent as commands, just like sending HTTP requests.

To implement Stripe Checkout, we can declare our Main module as a port module, and declare two ports for our page:

port module Page.Account.Main exposing (main)
{- imports here -}
port openPaymentForm : () -> Cmd msg
port createSubscription : (Encode.Value -> msg) -> Sub msg

openPaymentForm is an outgoing port, which will tell our JavaScript code to open the Stripe Checkout payment form. createSubscription is an incoming port: JavaScript code will send us messages through this port when the

These are just declarations, though. Elm will synthesize implementations to handle the interaction with the JavaScript code. But even so, these are just pure functions as they are. They won’t do anything until we use them in our app.

Before we can do that, we’ll need two messages in our Message type:

type Message
= ...
| OpenPaymentForm
| CreateSubscription Encode.Value
| ...

OpenPaymentForm gives our view a way to send the openPaymentForm command. CreateSubscription is the message we’ll receive with payment data from Stripe’s JavaScript.

We now need to add cases for these messages to our update function:

update message model =
case message of
...
OpenPaymentForm ->
( model, openPaymentForm () )
CreateSubscription value ->
handleCreateSubscription value model
...

The OpenPaymentForm message just fires off the openPaymentForm command for our port. Handling the CreateSubscription message is more complex, mostly due to having to decode JSON data coming from JavaScript:

paymentDecoder : Decoder ( String, String )
paymentDecoder =
Decode.map2 Tuple.pair
(Decode.field "email" Decode.string)
(Decode.field "id" Decode.string)
handleCreateSubscription : Encode.Value -> Model -> ( Model, Cmd Message )
handleCreateSubscription value model =
case Decode.decodeValue paymentDecoder value of
Ok ( email, token ) ->
( model
, Http.send SubscriptionCreated
(Request.User.createSubscription email token)
)
Err _ ->
( model, Cmd.none )

We create a JSON decoder for the data we expect to receive from our createSubscription port. In this case, it’s a JSON object with two fields we care about: id and email. These are provided to us by Stripe Checkout. We decode these into a pair of values, and then send off an HTTP request to our backend server to create the subscription using the information we got from Stripe. The implementation of Request.User.createSubscription isn’t super important here: it’s normal Elm HTTP request code.

Finally, in order to actually receive CreateSubscription messages, we need to add the port to our page’s subscriptions:

subscriptions model =
createSubscription CreateSubscription

Note that we provide a constructor function to the port to be able to build a message of our app’s message type from the provided JSON-encoded value coming in from the port.

That’s all we have to do on the Elm side of things, but of course, nothing will happen from this until we actually write some JavaScript.

Sprinkle in some JavaScript

Stripe Checkout actually supports being used in two different ways. The first is what we did in Elm 0.18: create a form and embed a script inside it that will wire up some events for us. We could technically still use that here, but there’s a much cleaner way now that we’re in a position to write a little bit of JavaScript.

Stripe Checkout provides a small JavaScript API to both open a payment form and define how to handle the generated payment token. Those two things correspond to the ports we declared for our Elm page, so let’s wire those together:

const stripeKey = "<your stripe publishable key>";
const flags = { stripeKey };
const app = Elm.Page.Account.Main.init({ flags });
const handler = StripeCheckout.configure({
key: stripeKey,
locale: "auto",
zipCode: true,
token(token) {
app.ports.createSubscription.send(token);
}
});
app.ports.openPaymentForm.subscribe(() => {
handler.open({
name: "My Product",
description: "Description of my product",
amount: 500
});
});

Each port we declared in our Main module becomes a JavaScript object on app.ports. For a port going from Elm to JavaScript, like openPaymentForm, we can use the subscribe method in JavaScript to be notified when the command is sent from Elm. We use this to tell Stripe Checkout to open the payment form.

For a port going from JavaScript to Elm, like createSubscription, we use the send method to feed data into the subscription on the Elm side. We use this in the token callback we provide to Stripe Checkout, which is called when the user submits the payment form. This gives us the data we need to be able to tell our Elm app to create a new subscription for the user.

That’s it! That’s all the JavaScript we need to write for this.

Is this an improvement?

We may have been forced to make this change due to new restrictions in Elm, but I think it’s worth asking: is our code better than when we started? Yes it is! Despite being more code, I think this Elm 0.19 solution is better than the Elm 0.18 version for a number of reasons:

  • There is no DOM manipulation happening outside of Elm.
  • All of our page interactions are going through Messages and Commands.
  • Elm gets to handle the API request for subscribing, rather than having to use an HTML form submission.
  • The separation of concerns is clearer, as the view code no longer needs to know how the payment form is opened or where to submit it.

The main benefit here is consistency. Before this change, getting payment info was a huge special case in the app, implemented completely differently from things that should have been similar. Now the implementation is much more like any other interaction happening in the app. The small piece that needs to be different is tucked away in a little JavaScript behind a small façade. That’s a huge improvement.

Having done it both ways, I would recommend using the ports approach even if you are still using Elm 0.18. It’s a better design that I think will serve you well, and it shouldn’t require any changes from what I’ve described here.