Stripe Recurring Billing Part 2

Assumptions


1) The user is already logged in.
2) The user model will have email field.
3) You can define : User has_one :subscription, Subscription belongs_to :user after you integrate with Devise, Authlogic etc.
4) Move access over the network to a background job in a later version.
5) To make the demo simple, the stripe related code is in the application wide layout. This should be moved to a separate app/views/layouts/subscriptions.html.erb layout file. Use this layout in subscriptions controller by using the Rails layout method.
6) The subscription page must be loaded using https.

Steps


Step 1

Create a Rails 4.2.4 project.

Step 2

Turn off Coffeescript, in application.rb:

config.generators.javascripts = false

Step 3

Add :

gem 'stripe' 

to Gemfile. Run bundle.

Step 4

Setup Stripe keys. In config/initializers/stripe.rb:

Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key:      ENV['STRIPE_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]
STRIPE_PUBLIC_KEY = Rails.configuration.stripe[:publishable_key]

Step 5

Login to your stripe account. Copy the API Keys and set Stripe environment variables on your development machine.

export STRIPE_PUBLISHABLE_KEY='pk_test_your publishable key' 
export STRIPE_SECRET_KEY='sk_test_your secret key' 

You can add it to your ~/.bash_profile file.

Step 6

Go to Rails console and type:

Stripe.api_key = Rails.configuration.stripe[:secret_key] 

Verify the value of secret_key for the Stripe.api_key variable is initialized correctly.

Step 7

In the application.html.erb file, within the head tag add:

<%= javascript_include_tag 'https://js.stripe.com/v2/' %>

Refer Building a Custom Payment Form for more explanation. The final layout file looks like this:

<!DOCTYPE html>
<html>
<head>
  <title>Striped</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

  <%= javascript_include_tag 'https://js.stripe.com/v2/' %>
  <%= tag :meta, :name => "stripe-key", :content => STRIPE_PUBLIC_KEY %>

  <script type="text/javascript">
    // This identifies your website in the createToken call below
    Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));

    var stripeResponseHandler = function(status, response) {
      var $form = $('#payment-form');

      if (response.error) {
        // Show the errors on the form
        $form.find('.payment-errors').text(response.error.message);
        $form.find('button').prop('disabled', false);
      } else {
        // token contains id, last4, and card type
        var token = response.id;
        // Insert the token into the form so it gets submitted to the server
        $form.append($('<input type="hidden" name="stripeToken" />').val(token));
        // and re-submit
        $form.get(0).submit();
      }
    };

    jQuery(function($) {
      $('#payment-form').submit(function(event) {
        var $form = $(this);

        // Disable the submit button to prevent repeated clicks
        $form.find('button').prop('disabled', true);

        Stripe.card.createToken($form, stripeResponseHandler);

        // Prevent the form from submitting with the default action
        return false;
      });
    });

  </script>


  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Here the meta-tag stripe-key defines the Stripe public key. Refer railscasts for more details.

Step 8

Run the server and go to localhost:3000/subscriptions/new. Enter 4242424242424242 for the credit card. Click on the 'Submit Payment' button. You will get the error: No route matches [POST] /subscriptions/new in the browser. Login to your Stripe account, in the test mode, click on the Logs under the REQUESTS tab. You should see that your request has reached Stripe servers.

Step 9

Change routes.rb:

post 'subscriptions/create' => 'subscriptions#create'

Step 10

Change create action in subscriptions controller:

  def create
    logger.info "*" * 80
    logger.info params
    logger.info "*" * 80
  end

In the log file we see:

 Parameters: {"stripeToken"=>"tok_4xfQ0TwMvzJIh9"}
Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1m

You will see : ActionController::InvalidAuthenticityToken error in the browser.

Step 11

Add the authenticity_token in the subscriptions/new.html.erb file:

<%= hidden_field_tag :authenticity_token, form_authenticity_token -%>

You can now see the stripe token in the log file:

********************************************************************************
{"authenticity_token"=>"9v7mQSZPHGn2CMn3TBBbIfCa8I5SzbPHQm+LMOh2kMw=", "stripeToken"=>"tok_4xfbyn7meV5eUH", "controller"=>"subscriptions", "action"=>"create"}
********************************************************************************

Step 12

You could put the current_user.id and stripeToken in a background job and process subscription later. For simplicity, let's create a Stripe::Customer object with current_user.email, plan_id and the stripeToken that we get from Stripe response.

Instead of creating a Stripe::Customer, retrieving it and then creating a subscription (two network calls), we can create the customer and subscription object at once by using the optional plan attribute in the Stripe::Customer class. Like this:

customer = Stripe::Customer.create(description: "This must be current_user.email", card:  params[:stripToken], plan: Subscription::GOLD)

The plan should be based on the plan selected by the current_user. This could happen when they click on a specific pricing plan. For now, it is hard-coded to the plan that is already created in the test account.

Step 13

There is no need to validate email because the current_user.email will have value. The email validation will be done when the user creates an account.

customer = Stripe::Customer.create(description: "This must be current_user.email", card:  params[:stripToken], plan: Subscription::GOLD)
puts "Save #{customer.id} as the stripe_customer_token in Subscription model"

To make code robust, handle Stripe related exceptions. For example, if the plan does not exist you will get: Stripe::InvalidRequestError : No such plan: gold error. The plan is case sensitive.

Step 14

Move the subscription creation to app/gateways/stripe_gateway.rb.

Step 15

Create the subscription model and migrate the database.

$rails g model subscription plan_name:string stripe_customer_token:string
$rake db:migrate

Step 16

  def create
    stripe = StripeGateway.new(Rails.logger)
    @subscription = stripe.create_subscription(email, stripe_token, plan_id)
  end

StripeGateway constructor takes a logger object, since it should not have any dependency on web application framework. It depends only on the Logger interface.

Step 17

Display the feedback message in subscriptions/create.html.erb.

<% if @subscription.complete? %>
You have been subscribed to <%= @subscription.plan_display_name %>.
<% end %>

You can view the details of the newly created customer in Stripe account by clicking the Customers tab.

Tip : If you have problem with disabled 'Submit Payment' button, in FireFox press the shift and refresh button to reload the page.

Complete Source Code


Here is the complete source code:

subscriptions_controller.rb

class SubscriptionsController < ApplicationController
  def new
  end

  def create
    stripe = StripeGateway.new(Rails.logger)
    @subscription = stripe.create_subscription('current_user.email', params[:stripeToken], Subscription::GOLD)
  end

end

routes.rb

Rails.application.routes.draw do
  get 'subscriptions/new'
  post 'subscriptions/create' => 'subscriptions#create'
end

application.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Striped</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

  <%= javascript_include_tag 'https://js.stripe.com/v2/' %>
  <%= tag :meta, :name => "stripe-key", :content => STRIPE_PUBLIC_KEY %>

  <script type="text/javascript">
    // This identifies your website in the createToken call below
    Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));

    var stripeResponseHandler = function(status, response) {
      var $form = $('#payment-form');

      if (response.error) {
        // Show the errors on the form
        $form.find('.payment-errors').text(response.error.message);
        $form.find('button').prop('disabled', false);
      } else {
        // token contains id, last4, and card type
        var token = response.id;
        // Insert the token into the form so it gets submitted to the server
        $form.append($('<input type="hidden" name="stripeToken" />').val(token));
        // and re-submit
        $form.get(0).submit();
      }
    };

    jQuery(function($) {
      $('#payment-form').submit(function(event) {
        var $form = $(this);

        // Disable the submit button to prevent repeated clicks
        $form.find('button').prop('disabled', true);

        Stripe.card.createToken($form, stripeResponseHandler);

        // Prevent the form from submitting with the default action
        return false;
      });
    });

  </script>


  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

config/initializers/stripe.rb

Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key:      ENV['STRIPE_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]
STRIPE_PUBLIC_KEY = Rails.configuration.stripe[:publishable_key]

app/gateways/stripe_gateway.rb

class StripeGateway
  def initialize(logger)
    @logger = logger
  end

  def create_subscription(email, stripe_token, plan_id)
    begin
      customer = Stripe::Customer.create(description: email, card: stripe_token, plan: plan_id)
      subscription = Subscription.new
      subscription.stripe_customer_token = customer.id
      subscription.plan_name = plan_id
      subscription.save!
      subscription
    rescue Stripe::InvalidRequestError => e
      @logger.error "Create subscription failed due to : #{e.message}"
    rescue Exception => ex
      @logger.error "Create subscription failed due to : #{ex.message}"  
    end
  end

end

application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Striped
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    # Turn off Coffeescript
    config.generators.javascripts = false    
  end
end

app/models/subscription.rb

class Subscription < ActiveRecord::Base
  GOLD = 2

  def plan_display_name
    case self.plan_name
    when 1
      'Silver'
    when 2
      'Gold'
    when 3
      'Platinum'
    else
      'Unknown'
    end
  end

  def complete?
    self.stripe_customer_token.present?
  end
end

migration file:

class CreateSubscriptions < ActiveRecord::Migration
  def change
    create_table :subscriptions do |t|
      t.string :plan_name
      t.string :stripe_customer_token

      t.timestamps
    end
  end
end

You can download the entire source code from Bitbucket: git@bitbucket.org:bparanj/striped.git.

References


  1. https://gist.github.com/briancollins/6365455
  2. https://stripe.com/docs/tutorials/forms
  3. Mastering Modern Payments by Pete Keen
  4. Billing with Stripe
  5. Creating a Form for Handling Payments with Stripe
  6. Subscriptions javascript


Related Articles


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.