October 11, 2016

Subscriptions with your Rails app and Stripe

Whilst you are writing your SaaS application, you are most likely driving towards what is probably the holy grail of payments, monthly subscriptions and recurring revenue. Of course this issue with this is that you need to write a bunch of code to make sure that people are getting what they have paid for, and people that don't pay you, well, don't. Even at it's most simple, this is still code that needs to be written, and even worse, maintained.

For you I have some excellent news. You don't have to do this. Stripe will do this for you.

By doing your payments with Stripe you have an entire suite of subscription software already at your disposal via their API, we'll just need to store a bit of information about the subscriptions, but largely, Stripe will take care of the rest. Let's take a look at setting it up.

Plans

In order for you to have subscribers, you need to have plans for them to subscribe to. You can create this on the Stripe dashboard, but you can also do it via the API. I prefer this method of doing it as you can create your local copy of the plan in your database as well, and this can save extra unnecessary calls to Stripe for information to display during signup. A great way to do this is to use your db/seeds.rb file and a local table to hold some information for us to use.

The database migration:

class CreatePlans < ActiveRecord::Migration
  def change
    create_table :plans do |t|
      t.string :stripe_id, null: false
      t.string :name, null: false
      t.decimal :display_price, null: false

      t.timestamps
    end
  end
end

and the db/seeds.rb

plan = Stripe::Plan.create(
  :amount => 2900,
  :interval => 'month',
  :name => 'Basic Plan',
  :currency => 'aud',
  :id => 'basic'
)

Plan.create(name: plan.name, stripe_id: plan.id, display_price: (plan.amount.to_f / 100))

Stripe::Plan.create(
  :amount => 4900,
  :interval => 'month',
  :name => 'Gold Plan',
  :currency => 'aud',
  :id => 'gold'
)

Plan.create(name: plan.name, stripe_id: plan.id, display_price: (plan.amount.to_f / 100))

Now we can run:

bundle exec rake db:seed

and the application and the Stripe account will have the information that we need to show some basics about the subscription plan to potential users who might want to sign up.

Subscribers table

Of course that means we need to be able to keep track of our subscribers. Depending on your system this may well be users, customers, what have you, but for this example I am just going to call the class Subscriber, and used this migration to create it:

class CreateSubscribers < ActiveRecord::Migration
  def change
    create_table :subscribers do |t|
      t.string :email, :password_digest, null: false
      t.string :stripe_customer_id, null: false
      t.datetime :subscribed_at, :subscription_expires_at, null: false
      t.integer :plan_id, null: false

      t.timestamps
    end

    add_index :subscribers, :plan_id, name: "plans_for_subsribers"
    add_index :subscribers, :subscribed_at, name: "subscribed_at_for_subscribers"
    add_index :subscribers, :subscription_expires_at, name: "expiring_subscritions_on_subscribers"
  end
end

And this can give us a really really simple Plan and Subscriber classes:

class Plan < ActiveRecord::Base

  has_many :subscribers

end
class Subscriber < ActiveRecord::Base

  has_secure_password

  belongs_to :plan

end
Taking Subscriptions

Now that the application knows about plans and knows about subscribers, we need to be able to have people sign up. This is really the same as one of my previous posts about Taking payments with Stripe but let's quickly run through it.

Stripe API Keys

Add your Stripe credentials to your app, making sure to use either your test or live credentials as required, I have them in a .env file that is loaded by dotenv

STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
STRIPE_SECRET_KEY=sk_test_your_key_here

And making the publishable key available to stripe.js in your application layout

<!DOCTYPE html>
<html>
<head>
  <title>StripeExample</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <script type="text/javascript" src="https://js.stripe.com/v2/"></script>
  <script type="text/javascript">
    Stripe.setPublishableKey('<%= STRIPE_PUBLIC_KEY %>');
  </script>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Which brings us to the form for subscription. First we need a simple controller for this resource:

class SubscribersController < ApplicationController

  def new
    @plan = Plan.first
    @subscriber = Subscriber.new
  end

end

In your own app, you will want to have a way for the subscriber to be able to choose what plan they wish to sign up to, but I don't care what Plan they are signing up to in this example, just that they can.

Now onto the form. This is the simplest form I could make, didn't even style it, but it will let us create a form at subscribers/new

<h2>Subscribe to the <%= @plan.name %></h2>

<%= form_for @subscriber do |f| %>

  <p>
    <%= f.label :email %>
    <%= f.text_field :email %>
  </p>

  <p>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </p>

  <p>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </p>

  <p>
    <%= label_tag "Amount" %>
    <%= number_to_currency(@plan.display_price) %>
  </p>

  <span class="payment-errors"></span>

  <p>
    <%= label_tag "Card Number"%>
    <%= text_field_tag nil, nil, data: { stripe: "number" } %>
  </p>

  <p>
    <%= label_tag "Expiration (MM/YY)" %>
    <%= text_field_tag nil, nil, data: { stripe: "exp_month" }, size: 4 %>
    <span>/</span>
    <%= text_field_tag nil, nil, data: { stripe: "exp_year" }, size: 4 %>
  </p>

  <p>
    <%= label_tag "CVC"%>
    <%= text_field_tag nil, nil, data: { stripe: "cvc" }, size: 4 %>
  </p>

  <%= f.submit "Submit Payment", class: "submit" %>

<% end %>

As in the previous post, we have to hijack the form submission, have stripe try to return a token and then continue on to submitting the form to the subscribers controller. Here is the JS again, but for a more detailed look at it, refer back to Taking a payment with Rails and Stripe, this is in subscribers.js

$(function() {
  var $form = $('#new_subscriber');
  $form.submit(function(event) {
    // Disable the submit button to prevent repeated clicks:
    $form.find('.submit').prop('disabled', true);

    // Request a token from Stripe:
    Stripe.card.createToken($form, stripeResponseHandler);

    // Prevent the form from being submitted:
    return false;
  });
});

function stripeResponseHandler(status, response) {
  // Grab the form:
  var $form = $('#new_subscriber');

  if (response.error) { // Problem!

    // Show the errors on the form:
    $form.find('.payment-errors').text(response.error.message);
    $form.find('.submit').prop('disabled', false); // Re-enable submission

  } else { // Token was created!

    // Get the token ID:
    var token = response.id;

    // Insert the token ID into the form so it gets submitted to the server:
    $form.append($('<input type="hidden" name="stripeToken">').val(token));

    // Submit the form:
    $form.get(0).submit();
  }
};
Create the Subscription

Once the signup form has made it off to Stripe, returned us a token and made it back to us, we need a create action in our SubscribersController to be able to handle it, like so:

class SubscribersController < ApplicationController
  def new
    @plan = Plan.first
    @subscriber = Subscriber.new
  end

  def create
    @plan = Plan.first
    @subscriber = Subscriber.new(permitted_params)
    stripe_token = params[:stripeToken]
    if @subscriber.save_and_make_payment(@plan, stripe_token)
      redirect_to subscribers_path
    else
      render :new
    end
  end

  private

    def permitted_params
      params.require(:subscriber).permit(:email, :password, :password_confirmation)
    end

end

Which then allows our Subscriber class to work with Stripe:

class Subscriber < ActiveRecord::Base

  has_secure_password

  belongs_to :plan

  def save_and_make_payment(plan, card_token)
    self.plan = plan
    if valid?
      begin
        customer = Stripe::Customer.create(
          source: card_token,
          plan: plan.stripe_id,
          email: email,
        )
        self.stripe_customer_id = customer.id
        save(validate: false)
      rescue Stripe::CardError => e
        errors.add :credit_card, e.message
        false
      end
    else
      false
    end
  end

end

In this code we are assigning our plan, to be saved locally in the app for reference, then we are using our card_token that we have gotten from stripe.js and creating a customer, saving the token as the payment source, and saving the stripe plan id as well. If this is successful Stripe will return us a customer object with a bunch of information, but what we want is the customer.id part, and saving this on our subscriber will allow us to update our subscriber record with any information that Stripe sends us about them. How do they do that? I'm glad you asked.

Webhooks

Stripe can send your application a whole host of information about your account, it's subscribers, events to do with them, loads of stuff. For us right now what we are interested in subscription expiration dates in case we need to restrict access to parts of the application based on subscription status. To do this we need to setup a webhook in your Stripe account. You can do that here. When you get there click on Add Endpoint, fill in the url:

https://example.com/stripe/webhooks

Choose test mode and just all events for now.

Receive info from stripe

So now we need to setup this webhook to receive information from Stripe. First it needs a route:

Rails.application.routes.draw do

  resources :subscribers
  post '/stripe/webhooks', to: "stripe#webhooks"

end

And also a controller that will receive the info:

class StripeController < ApplicationController

  protect_from_forgery :except => :webhooks

  def webhooks
    event_json = JSON.parse(request.body.read)
    event = Stripe::Event.retrieve(event_json["id"])

    if event.type =~ /^customer.subscription/
      subscriber = Subscriber.find_by(stripe_customer_id: event.data.object.customer)
      subscriber.subscribed_at = Time.at(event.data.object.start).to_datetime
      subscriber.subscription_expires_at = Time.at(event.data.object.current_period_end).to_datetime
      subscriber.save
    end

    render nothing: true
  end

end

As this request is coming from Stripe and not our own application, we can skip the protect_from_forgery for our webhooks. This is not something that I would usually suggest, or recommend, but as we can receive the data from Stripe but then call back to them to make sure that this is indeed a Stripe Event, I feel a little better about this. That is done with this code:

event_json = JSON.parse(request.body.read)
event = Stripe::Event.retrieve(event_json["id"])

Once we have verified that it is indeed a Stripe event we can do some stuff with the data. My code is checking for event.type as we are only interested in subscription events at this time. If it is our subscription event, we can go ahead and look up the Subscriber by their Stripe customer id and update them with the time they subscribed at, and when that subscription is going to expire. We can then use that subscription expiration date combined with the plan that they are on to decide what they have access to inside the application.

Stripe Events

What we've built here is the very simple foundation of a Stripe subscription and some data syncing via webhooks. The control you can have over the webhooks is quite fine grained, and you can go quite far with setting up several different hooks to deal with different events that might be relevant to your applications, e.g. receiving invoices for customers, deleting plans and customers (deactivating?) if they are deleted in the Stripe dashboard and many many more. Whilst there are many different webhooks you could build, they all follow the same principle.

I hope that this was helpful, and if you have any questions, comments, feedback, please leave a comment, or if you'd prefer send me and email or hit me up on twitter.