Rader on Rails

Dispatches from my web development journey.

A Stripe Integration

Another monstrous blog post. I promise these will get shorter in the future.

For my Bloc project challenge, I was tasked with creating a simple social wiki application that, among other things, allowed users to select either a free or premium plan upon signup. As this suggests, free users have limited access to certain features, and premium users have to enter their credit card information and be charged at a monthly rate to access expanded features. Bloc recommended using Stripe, an awesome payments solution for SaaS apps.

I’ve never done anything like this before, so there was a fair amount of struggle involved. I’m going to cover the solution I implemented.

(Quick note, before I go further, the final result of the overall project can be accessed here: http://rader-blocipedia.herokuapp.com, source code: http://github.com/raderj89/blocipedia)

So first, I knew every user would belong to one of two plans, Free or Premium. Thus, I knew I would have to create a Plans table in my database with the appropriate relations between it and users. In Rails, I can simply run rails g model Plan and pass in the data columns I want, in this case: name:string price:decimal. This produces the migration:

1
2
3
4
5
6
7
8
9
10
class CreatePlans < ActiveRecord::Migration
  def change
    create_table :plans do |t|
      t.string :name
      t.decimal :price

      t.timestamps
    end
  end
end

And a model, plan.rb:

1
2
3
class Plan < ActiveRecord::Base
  attr_accessible :name, :price
end

Now, how to associate? Rails makes this really easy by letting you declare very intuitive associations in the model itself. As I said before, many users will belong to a plan, thus a plan has many users. How do you let your application know this? By adding one line: has_many :users.

So now, plan.rb:

plan.rb
1
2
3
4
class Plan < ActiveRecord::Base
  attr_accessible :name, :price
  has_many :users
end

I want to go ahead and set the specific plans in my app, and I can do this by seeding the database. All that’s needed is to specify the plans in seeds.rb:

seeds.rb
1
2
Plan.create!(name: "Free", price: 0)
Plan.create!(name: "Premium", price: 5)

And now inside my user model, I also need to let my app know that a user belongs_to :plan. Now some of you might have read this and thought to yourself, “Each user has one plan,” and other Rails beginners know that has_one is a valid association. So can I use has_one in this situation?

Not necessarily. There’s an important difference that Rails defines between these two, and the Rails docs explain it perfectly:

If you want to set up a one-to-one relationship between two models, you’ll need to add belongs_to to one, and has_one to the other. How do you know which is which? The distinction is in where you place the foreign key (it goes on the table for the class declaring the belongs_to association), but you should give some thought to the actual meaning of the data as well. The has_one relationship says that one of something is yours – that is, that something points back to you.

Once you think of it like this, it becomes clear why you’d need to declare that a user belongs_to :plan, because the plan doesn’t just point back to your account, it points anyone with that account.

Notice I’m using Devise for authentication:

user.rb
1
2
3
4
5
6
7
8
9
10
11
12
class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable #:confirmable,

  attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :plan_id

  has_many :wikis, dependent: :destroy
  has_many :wiki_collaborations
  has_many :shared_wikis, through: :wiki_collaborations, source: :wiki

  belongs_to :plan
end

I won’t cover the wiki associations, but you’ll see that those are in there too. Can you guess what those associations do?

Now, my user model will know that each user belongs to a plan. To actually connect the user to the plan, as the passage from the Rails docs said, I need to store a foreign key relating to the specific plan on the user. To do that, I create a simple migration: rails g migration add_plan_to_user plan_id:integer, which generates:

1
2
3
4
5
class AddPlanToUser < ActiveRecord::Migration
  def change
    add_column :users, :plan_id, :integer
  end
end

Now I have a column in the users database that references which plan the user belongs to.

Once I had this set up and working, I began thinking about how I wanted users to sign up.

Devise gives you views that you can customize to your liking, including registration views. I wanted the free form to not display any credit card information (and not interact with Stripe at all), and the premium form to display credit card info and subscribe the user to my premium plan.

At this point, I knew I was going to have to do a bit of research. Reading Stripe’s docs is always good, but for something a little quicker and easier to digest, I watched Ryan Bates’ Railscast on using Stripe. This provided a great starting point for me to start using Stripe and understanding all that was going on. I recommend watching the episode, because Ryan and Railscasts are awesome, and because I’ll skip over some of the finer details that he covers well.

The main great thing about Stripe is that it helps you remain PCI Compliant when handling transactions by providing a JavaScript library that prevents any credit card information from touching your application, mitigating the risk of malicious forces intercepting the info.

In Ryan’s solution, he provides a handy jQuery function (written in CoffeeScript) to obtain credit card values that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
jQuery ->
  Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'))
  subscription.setupForm()

subscription =
  setupForm: ->
    $('#new_subscription').submit ->
      $('input[type=submit]').attr('disabled', true)
      if $('#card_number').length
        subscription.processCard()
        false
      else
        true

  processCard: ->
    card =
      number: $('#card_number').val()
      cvc: $('#card_code').val()
      expMonth: $('#card_month').val()
      expYear: $('#card_year').val()
    Stripe.createToken(card, subscription.handleStripeResponse)

  handleStripeResponse: (status, response) ->
    if status == 200
      $('#subscription_stripe_card_token').val(response.id)
      $('#new_subscription')[0].submit()
    else
      $('#stripe_error').text(response.error.message)
      $('input[type=submit]').attr('disabled', false)

Check out the Railscast to learn exactly what this does. In the most basic sense, once the page has loaded, Stripe is prepared to accept a new payment. Once a user enters her credit card info and hits submit, the code processes the card, creating a token that Stripe needs to create a new customer. And there are error handlers inside the code as well.

To save the user to Stripe, and to let our application know the user is now a paying customer, we create a special save method that saves the payment information to our Stripe account, and Stripe gives us a customer ID that we can then save to our database. In Ryan’s example, he doesn’t save users, just subscriptions, and this is where things diverged for me. In my case, I had a user model generated with the Devise library, which comes with all of its own CRUD actions.

To accomplish saving the new subscriptions, Ryan created a save_with_payment method in his subscription model:

1
2
3
4
5
6
7
def save_with_payment
  if valid?
    customer = Stripe::Customer.create(description: email, plan: plan_id, card: stripe_card_token)
    self.stripe_customer_token = customer.id
    save!
  end
end

And in his subscriptions controller, he can simply call this method in place of the normal save action: subscription.save_with_payment.

But I need to call this action when saving a user, which is being handled by Devise. So I’m going to need to override some of Devise’s methods.

To do this, I need to create my own registrations controller that inherits from the Devise registrations controller. I can set this up like this:

1
2
3
class Users::RegistrationsController < Devise::RegistrationsController

end

Devise provides default views for signing in, registrations and editing profiles. Because I want people to select a plan upon sign up, my form needed to allow the user to select a plan, and if it is the free plan, to not show credit card information, and if it’s the premium plan, to display the credit card fields.

To make this easier, I followed how many SaaS apps integrate plan selection with the sign-up process. I decided to put two buttons on my home page that would pass in the specific plan number to the sign in form:

views/pages/home.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  <div class="row">
    <div class="col-sm-5">
      <h3>Free Account</h3>
      <ul>
        <li>Create unlimited public wikis</li>
        <li>Collaborate with unlimited users</li>
      </ul>
      <%= link_to "Sign up now!", sign_up_path(plan: @free_plan.id), class: 'btn btn-default btn-lg btn-block' %>
    </div>
    <div class="col-sm-5 col-sm-offset-2">
      <h3>Premium Account</h3>
      <ul>
        <li>All the benefits of basic</li>
        <li>Unlimited private wikis</li>
      </ul>
      <%= link_to "Sign up now!", sign_up_path(plan: @premium_plan.id), class: 'btn btn-success btn-lg btn-block' %>
    </div>
  </div>
  <% end %>
</div>

I defined those instance variables in the pages controller:

pages_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PagesController < ApplicationController
  before_filter :setup, only: [:home]

  def home
    @free_plan
    @premium_plan
    @wikis = Wiki.paginate(page: params[:page], per_page: 10)
  end

  private

  def setup
    plans = Plan.all
    plans.each do |plan|
      if plan.id == 1
        @free_plan = plan
      else
        @premium_plan = plan
      end
    end
  end
end

You’ll notice I created a private method that loops over each plan, assigning them to either @free_plan or @premium_plan, which I can then include in the home method.

Once they select a plan that they want to sign up with, it takes them to a the default Devise registration form that includes the user’s selected plan in a hidden field tag and some conditional logic that prompts the user to input credit card information if the premium plan is selected.

sign-up form in views/devise/registrations/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<div class="panel panel-default">
  <div class="panel-heading">
    <% if params[:plan] == "2" %>
    <h1>Sign up with premium!</h1>
  </div>

  <div class="panel-body">
      <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
        <%#= devise_error_messages! %>

        <%= hidden_field_tag 'plan', params[:plan] %>

        <div class="form-group">
          <%= f.label :username %>
          <%= f.text_field :username, autofocus: true, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: "form-control" %>
        </div>

        <h2>Payment</h2>

        <%= f.hidden_field :stripe_card_token %>
          <div class="form-group">
            <%= label_tag :card_number, "Credit Card Number" %>
            <%= text_field_tag :card_number, nil, name: nil, class: "form-control" %>
          </div>
          <div class="form-group">
            <%= label_tag :card_code, "Security Code on Card (CVV)" %>
            <%= text_field_tag :card_code, nil, name: nil, class: "form-control" %>
          </div>
          <div class="form-group">
            <%= label_tag :card_month, "Card Expiration" %>
            <%= select_month nil, {add_month_numbers: true}, {name: nil, id: "card_month"}%>
            <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+15}, {name: nil, id: "card_year"}%>
          </div>
          <div id="stripe_error">
            <noscript>JavaScript is not enabled and is required for this form. First enable it in your web browser settings.</noscript>
          </div>

        <div class="form-group">
          <%= f.submit "Sign up", class: "btn btn-lg btn-success" %>
        </div>
      <% end %>
  </div>
  <% else %>
  <h1>Sign up for free</h1>
  </div>
  <div class="panel-body">
   <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), html: { id: "free_user"}) do |f| %>

        <%= hidden_field_tag 'plan', params[:plan] %>

        <div class="form-group">
          <%= f.label :username %>
          <%= f.text_field :username, autofocus: true, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.submit "Sign up", class: "btn btn-lg btn-primary" %>
        </div>
      <% end %>
    <% end %>
  </div>
</div>

At first, I only had one form and had simply put an if statement around the credit card fields. However, this produced a problem: Each time I tried signing up for a free account, it would fire off the Stripe confirmation. That’s because the registration form, whether free or premium, was given the same default ID generated by Devise. To account for this, I simply created two forms in the same view, specifying my own ID for the free plan form so that it wouldn’t activate the Stripe confirmation. It felt like a bit of an ugly solution, and a bit against the DRY principle, but it worked.

Now, to handle the specific sign ups, I knew I’d have to tweak Devise’s default controller actions. After researching the Devise source code.

In Devise’s registrations controller, they define a build_resource method that builds a User model inside the new and create actions. To override Devise’s methods, all you need to do is define methods with the same name and include whatever code you want to be added. I tweaked the build_resource method a bit to specify a plan attribute for the user resource inside the new and create actions:

users/registrations_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  def new
    unless (params[:plan] == '1' || params[:plan] == '2')
      flash[:notice] = "Please select a plan to sign up."
      redirect_to root_url
    end
  end

  private

  def build_resource(*args)
    super
    if params[:plan]
      resource.plan_id = params[:plan]
      if resource.plan_id == 2
        resource.save_with_payment
      else
        resource.save
      end
    end
  end

  def setup
    plans = Plan.all
    plans.each do |plan|
      unless plan.id == 1
        @premium_plan = plan
      end
    end
  end
end

I use super to access all of the regular Devise code and then add my specifications on top. If plan parameters are passed in to the registration form, it sets the resource’s plan ID, and if the plan ID is ‘2’, then it calls the save_with_payment method on the resource. Otherwise, it simply saves the user with a free plan.

I also added code to Devise’s new method that ensures the plan parameters passed are only of plan ID “1” or “2” so that a person cannot pass into the URL a plan number that doesn’t correspond with any of the plans I’ve specified.

The next challenge was to allow free users to upgrade to a paid plan, and conversely, to allow premium users to downgrade to free.

First, upgrading to premium: Devise provides an edit user page. To handle this, I included a second form in the page allowing users to upgrade their plan (the form is the last code block below). Because free users aren’t signed up with Stripe at all, I decided I could simply use the same save_with_payment method I had defined before.

I then needed to define an action to handle updating the plan. I placed this in the registrations controller, and specified in my form (last code block below) to put the data into the users_update_plan_path.

update plan method in users/registrations_controller.rb
1
2
3
4
5
6
7
8
9
10
11
  def update_plan
    @user = current_user
    if (params[:user][:stripe_card_token] != nil) && (params[:plan] == "2")
      @user.update_attributes(plan_id: params[:plan], email: params[:email], stripe_card_token: params[:user][:stripe_card_token])
      @user.save_with_payment
      redirect_to edit_user_registration_path, notice: "Updated to premium!"
    else
      flash[:error] = "Unable to update plan."
      redirect_to :back
    end
  end

To ensure that this works correctly, the if statement checks that a Stripe card token is generated and that the plan selected is only 2. If that checks out, the user’s attributes are updated and I call the save_with_payment method on the user.

I also created a CoffeeScript file to handle the Stripe response, using the same code for the sign up action, but tweaking it to correspond to the update plan form.

I’m curious if there’s a better way to do this because technically, this goes against the principle that controller actions should be RESTful. I decided to just consider it semi-RESTful (it’s an update action after all).

Then, to downgrade from paid to free, I consulted the Stripe docs for canceling subscriptions and created a cancel_user_plan method in the user model. This is simple enough – all that’s required is telling Stripe to retrieve the customer ID, and call Stripe’s cancel_subscription method on the customer. So in the User model, I added a cancel_user_plan method, passing in the user’s customer ID as an argument:

cancel user plan in user.rb
1
2
3
4
5
6
7
8
  def cancel_user_plan(customer_id)
      customer = Stripe::Customer.retrieve("#{customer_id}")
      customer.cancel_subscription
    rescue Stripe::InvalidRequestError => e
      logger.error "Stripe error while deleting customer: #{e.message}"
      errors.add :base, "No active subscriptions for user."
      false
  end

I added a simple form with the user’s customer token included in a hidden field. When the user hits the ‘Cancel Subscription’ button, it passes the customer token to the cancel_plan action, which I added to the registrations controller:

cancel plan users/registrations_controller.rb
1
2
3
4
5
6
7
8
9
10
11
  def cancel_plan
    @user = current_user
    if @user.cancel_user_plan(params[:customer])
      @user.update_attributes(stripe_customer_token: nil, plan_id: 1)
      flash[:notice] = "Canceled subscription."
      redirect_to edit_user_registration_path
    else
      flash[:error] = "There was an error canceling your subscription. Please notify us."
      render :edit
    end
  end

Notice I pass params[:customer] to @user.cancel_user_plan. This is because I specified that the method would take an argument – the user’s Stripe customer token.

And here’s the form that handles these actions:

devise/edit.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<div class="panel panel-default">
  <div class="panel-heading">
    <div class="panel-title">
      <h1>Edit <%= resource_name.to_s.humanize %></h1>
    </div>
    Current account: <%= resource.plan.name %>
  </div>
  <div class="panel-body">
    <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
      <%= devise_error_messages! %>

      <div class="form-group">
        <%= f.label :email %>
        <%= f.email_field :email, class: "form-control", :autofocus => true %>
      </div>

      <div class="form-group">
        <%= f.label :password %> <i>(leave blank if you don't want to change it)</i>
        <%= f.password_field :password, class: "form-control", :autocomplete => "off" %>
      </div>

      <div class="form-group">
        <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i>
        <%= f.password_field :current_password, class: "form-control" %>
      </div>

      <div class="form-group">
        <%= f.submit "Update", class: "btn btn-primary" %>
      </div>
    <% end %>

  <hr>

  <% if resource.plan.name == "Free" %>

  <h2>Upgrade to paid</h2>
  <%= form_for(resource, :as => resource_name, :url => users_update_plan_path, :html => { id: 'upgrade-plan', :method => :put }) do |f| %>
    <%= hidden_field_tag 'plan', @premium_plan.id %>
    <%= hidden_field_tag 'email', resource.email %>
    <%= f.hidden_field :stripe_card_token, id: 'new-token' %>
    <div class="form-group">
      <%= label_tag :card_number, "Credit Card Number" %>
      <%= text_field_tag :card_number, nil, name: nil, class: 'form-control' %>
    </div>
    <div class="form-group">
      <%= label_tag :card_code, "Security Code on Card (CVV)" %>
      <%= text_field_tag :card_code, nil, name: nil, class: 'form-control' %>
    </div>
    <div class="form-group">
      <%= label_tag :card_month, "Card Expiration" %>
      <%= select_month nil, {add_month_numbers: true}, {name: nil, id: "card_month"} %>
      <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+15}, {name: nil, id: "card_year",} %>
    </div>
    <div id="stripe_error">
      <noscript>JavaScript is not enabled and is required for this form. First enable it in your web browser settings.</noscript>
    </div>

    <div class="form-group">
      <%= f.submit "Upgrade", class: "btn btn-lg btn-success" %>
    </div>
    <% end %>

  <% else %>

  <h2>Cancel Subscription</h2>
    <%= form_for(resource, :as => resource_name, :url => users_cancel_plan_path, :html => { id: 'cancel-plan', :method => :put }) do |f| %>

    <%= hidden_field_tag 'customer', resource.stripe_customer_token %>

    <%= f.submit "Cancel Subscription", class: "btn btn-danger" %>

    <% end %>

  <% end %>

  </div>

  <div class="panel-footer">
    <h3>Cancel my account</h3>

    <p><%= button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete, class: "btn btn-danger" %></p>
  </div>
</div>

Comments