This week we're going to handle Stripe integration for a fictional subscription service. Today, we'll be integrating with Stripe.js to generate a token from a credit card form. Let's get started.

Project

I'm starting on the elm_web_components_playground project, tagged before this episode.

Basic Billing Model Support

Since the last episode, I've just added a basic BillingModel and corresponding Msg and Update functions. Let's have a look briefly:

Model Updates

vim src/Model.elm
module Model
    exposing
        ( Model
        , BillingModel -- <--
        , CreditCardModel -- <--
        , initialBillingModel -- <--
        , initialCreditCardModel -- <--
        )
-- ...
type alias Model =
    { -- ...
    , billing : BillingModel
    }


-- Our BillingModel keeps track of a potential token response from the server,
-- and a credit card we're building in the form.
type alias BillingModel =
    { token : Maybe String
    , creditCard : CreditCardModel
    }


-- The credit card has typical fields
type alias CreditCardModel =
    { name : String
    , ccNumber : String
    , cvc : String
    , expiration : String
    , zip : String
    }


-- And we've added some basic functions to generate the initial versions of each
-- of these types.
initialBillingModel : BillingModel
initialBillingModel =
    { token = Nothing
    , creditCard = initialCreditCardModel
    }


initialCreditCardModel : CreditCardModel
initialCreditCardModel =
    { name = ""
    , ccNumber = ""
    , cvc = ""
    , expiration = ""
    , zip = ""
    }

We updated our initial app model to support the new billing field:

vim src/App.elm
module App exposing (..)
-- ...
init : Navigation.Location -> ( Model, Cmd Msg )
init location =
    ( { -- ...
      , billing = Model.initialBillingModel -- <--
      }
    , Cmd.none
    )

Billing Msg Handling

We also added a few new things to our Msg module:

vim src/Msg.elm
module Msg exposing (Msg(..), BillingMsg(..), CreditCardMsg(..))
-- ...
type Msg
    = -- ...
    | Billing BillingMsg


type BillingMsg
    = CreditCard CreditCardMsg
    | AskForToken
    | ReceiveToken String


type CreditCardMsg
    = SetName String
    | SetCcNumber String
    | SetCvc String
    | SetExpiration String
    | SetZip String

We of course needed to handle the new messages in our Update module:

vim src/Update.elm
module Update exposing (update)

import Model
    exposing
        ( Model
        , BillingModel
        , CreditCardModel
        , initialCreditCardModel
        )
import Msg exposing (Msg(..), BillingMsg(..), CreditCardMsg(..))
-- ...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        Billing billingMsg ->
            let
                ( billingModel, billingCmd ) =
                    updateBilling billingMsg model.billing
            in
                ( { model | billing = billingModel }
                , billingCmd
                )


updateBilling : BillingMsg -> BillingModel -> ( BillingModel, Cmd Msg )
updateBilling msg model =
    case msg of
        CreditCard creditCardMsg ->
            ( { model
                | creditCard =
                    updateCreditCard creditCardMsg model.creditCard
              }
            , Cmd.none
            )

-- When we ask for a token, we'll send our credit card model out of a port
        AskForToken ->
            ( model
            , Ports.askForToken model.creditCard
            )

        ReceiveToken token ->
            ( { model
                | token = Just token
                , creditCard = Model.initialCreditCardModel
              }
            , Cmd.none
            )


updateCreditCard : CreditCardMsg -> CreditCardModel -> CreditCardModel
updateCreditCard msg model =
    case msg of
        SetName name ->
            { model | name = name }

        SetCcNumber ccNumber ->
            { model | ccNumber = ccNumber }

        SetCvc cvc ->
            { model | cvc = cvc }

        SetExpiration expiration ->
            { model | expiration = expiration }

        SetZip zip ->
            { model | zip = zip }

This is all pretty standard fare for us at this point so outside of showing where we're at, I don't know that it needs that much discussion.

A New Port

We also added a port:

vim src/Ports.elm
port module Ports exposing (closeDrawer, askForToken)

import Model exposing (CreditCardModel)
-- ...
port askForToken : CreditCardModel -> Cmd msg

View Updates

Finally, we wire all of this into our View.Forms module, showing the model's data and emitting the appropriate Msgs:

vim src/View/Forms.elm
module View.Forms exposing (view)

import Html exposing (Html, text, div, node, h2, p, a)
import Html.Attributes exposing (attribute, style, class, href, value)
import Html.Events exposing (onClick, onInput)
import Msg
    exposing
        ( Msg(NewUrl, Billing)
        , BillingMsg(..)
        , CreditCardMsg(..)
        )
import Model exposing (Model)
import Polymer.Paper as Paper
import Polymer.Attributes exposing (label)
import Routes exposing (Route(DatePicker))


view : Model -> Html Msg
view model =
    let
        creditCard =
            model.billing.creditCard
    in
        div
            [ class "view-forms" ]
            [ Paper.card
                [ attribute "heading" "Billing Information"
                , attribute "elevation" "2"
                ]
                [ div [ class "card-content" ]
                    [ p [] [ text <| "Token: " ++ (toString model.billing.token) ]
                    , Paper.input
                        [ label "Name"
                        , value creditCard.name
                        , onInput <| Billing << CreditCard << SetName
                        , attribute "required" ""
                        , attribute "auto-validate" ""
                        , attribute "error-message" "I need a name"
                        ]
                        []
                    , node "gold-cc-input"
                        [ label "Credit Card Number"
                        , value creditCard.ccNumber
                        , onInput <| Billing << CreditCard << SetCcNumber
                        , attribute "required" ""
                        , attribute "auto-validate" "true"
                        , attribute "error-message" "This is not a valid credit card number"
                        ]
                        []
                    , node "gold-cc-cvc-input"
                        [ label "CVC"
                        , value creditCard.cvc
                        , onInput <| Billing << CreditCard << SetCvc
                        , attribute "required" ""
                        , attribute "auto-validate" ""
                        , attribute "error-message" "CVC is required"
                        ]
                        []
                    , node "gold-cc-expiration-input"
                        [ label "Expiration"
                        , value creditCard.expiration
                        , onInput <| Billing << CreditCard << SetExpiration
                        , attribute "required" ""
                        , attribute "auto-validate" ""
                        , attribute "error-message" "Expiration dates are important"
                        ]
                        []
                    , node "gold-zip-input"
                        [ label "Zip Code"
                        , attribute "required" ""
                        , attribute "auto-validate" ""
                        , attribute "error-message" "Please enter a valid zip code"
                        ]
                        []
                    ]
                , div [ class "card-actions" ]
                    [ Paper.button
                        []
                        [ text "Submit" ]
                    ]
                ]
            , Paper.card
                [ attribute "elevation" "2" ]
                [ p
                    [ class "card-content" ]
                    [ text "Neat, what else do you have?" ]
                , div
                    [ class "card-actions" ]
                    [ Paper.button
                        [ onClick <| NewUrl DatePicker ]
                        [ text "Next" ]
                    ]
                ]
            ]

With that, we've got the form reflecting our billing model and showing us our token. Now, let's move on to integrating with Stripe.

Ports.askForToken

Let's open up the JavaScript file and subscribe to the askForToken port:

vim src/index.js
// ...
window.addEventListener('WebComponentsReady', () => {
  // ...
  app.ports.askForToken.subscribe((creditCardModel) => {
    console.log(creditCardModel)
  })
  // ...
})

Now we need to wire up the Submit button on our Credit Card form to emit an AskForToken message:

vim src/View/Forms.elm
view : Model -> Html Msg
view model =
    let
        creditCard =
            model.billing.creditCard
    in
        div
            [ class "view-forms" ]
            [ Paper.card
                [ attribute "heading" "Billing Information"
                , attribute "elevation" "2"
                ]
                [ div [ class "card-content" ]
                    [ -- ...
                    -- Oops forgot to wire up zip!
                    , node "gold-zip-input"
                        [ label "Zip Code"
                        , value creditCard.zip
                        , onInput <| Billing << CreditCard << SetZip
                        , attribute "required" ""
                        , attribute "auto-validate" ""
                        , attribute "error-message" "Please enter a valid zip code"
                        ]
                        []
                    ]
                , div [ class "card-actions" ]
                    [ Paper.button
                        -- Add this onClick
                        [ onClick <| Billing <| AskForToken ]
                        [ text "Submit" ]
                    ]
                ]
            -- ...

So now our Submit button will send our CreditCardModel out of the askForToken port. If you fill out the form and click the button, you should see the model logged to the JavaScript console.

Stripe.js

Now let's send the data to Stripe. You have to load Stripe.js from their servers - it can't be bundled with webpack - so we'll add it to the index.html:

vim src/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- omitted for brevity -->
    <title>Elm and Web Components</title>
    <script type="text/javascript" src="https://js.stripe.com/v2/"></script>
  </head>
  <!-- omitted for brevity -->
</html>

Now we'll have a window.Stripe variable available, once the script loads. We'll assume it loads fine and that variable is available to us.

Next, we'll use Stripe.js to create a credit card when we receive our CreditCardModel from the port:

vim src/index.js
  // Set our stripe publishable key - yours will be different!
  Stripe.setPublishableKey('pk_test_ui9kge72Kvk3KHQnRYoRSPYf')
  app.ports.askForToken.subscribe((creditCardModel) => {
    Stripe.card.createToken({
      number: creditCardModel.ccNumber,
      cvc: creditCardModel.cvc,
      exp: creditCardModel.expiration,
      address_zip: creditCardModel.zip,
    }, stripeResponseHandler)
  })

  function stripeResponseHandler(status, response){
    console.log("got stripe data back!")
    console.log("status", status)
    console.log("response", response)
  }

If everything worked, you should see a response from Stripe that looks like this:

got stripe data back!
status 200
response
  {
    "id": "tok_19Sh10CwSsZX1HI7RFVOKirb",
    "object": "token",
    "card": {
      "id": "card_19Sh10CwSsZX1HI7GcGAyAeL",
      "object": "card",
      "address_city": null,
      "address_country": null,
      "address_line1": null,
      "address_line1_check": null,
      "address_line2": null,
      "address_state": null,
      "address_zip": "35242",
      "address_zip_check": "unchecked",
      "brand": "Visa",
      "country": "US",
      "cvc_check": "unchecked",
      "dynamic_last4": null,
      "exp_month": 11,
      "exp_year": 2017,
      "funding": "unknown",
      "last4": "1111",
      "metadata": {},
      "name": null,
      "tokenization_method": null
    },
    "client_ip": "XX.XX.XX.XX",
    "created": 1482214942,
    "livemode": false,
    "type": "card",
    "used": false
  }

That's it!

Summary

In today's episode, we saw how to create a token on Stripe using Stripe.js, by passing our CreditCardModel out of a port and writing a small amount of JavaScript glue code. Tomorrow we'll look at passing this token back into our Elm application and interacting with a backend API to complete a subscription signup flow. We'll also manage our Stripe token with Webpack so it can be swapped out for different builds. See you soon!

Resources