Skip to content

Instantly share code, notes, and snippets.

@tmcdb
Last active April 10, 2019 01:23
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tmcdb/a989e2f7a6a04dbef279 to your computer and use it in GitHub Desktop.
Save tmcdb/a989e2f7a6a04dbef279 to your computer and use it in GitHub Desktop.

Integrating Stripe with Rails

This written tutorial covers building a payment system into a Rails app using Stripe.js. It assumes a basic familiarity with Ruby on Rails, Git and the command-line.

There is a slightly contracted version of this tutorial available here.

What's covered

  • Setting up a basic Rails app with a scaffold generator
  • Integrating Stripe charges
  • Deploying to Heroku

What you'll need

Don't have what you need?

If you don't have any of the above follow this tutorial on setting up your environment for Ruby on Rails with Heroku. If you're unfamiliar with command-line basics this tutorial on the command-line for beginners will be helpful.

Contents


Payments with Stripe in Rails

Let's start by creating a basic Rails app. If you have an existing app for which you wish to integrate a payment system, feel free to skip these initial steps.


New project

Open up your preferred command line interface (Terminal, iTerm2, CommandPrompt) and navigate to a desired folder for coding. For exaple cd ~/Sites/rails_projects.

Once inside the folder, we can begin using Rails' generators. I'll be using the scaffold generator to save time. It generates a Model, a Migration, a Controller and Views – all replete with boilerplate code.

I'm calling the app Paymental (because I'm ker-azy like that).

$ rails new paymental
$ cd paymental

Immediately initialize your project as a Git repo, so you can track every change made to the codebase.

$ git init
$ git add .
$ git commit -m 'initial commit'

Push it to your prefered code host for collaboration and backup purposes.

$ git remote add origin git git@github.com:tmcdb/paymental.git
$ git push -u origin master

At this point it's normally a good idea to run the app and point your browser at the empty project's landing page.

$ rails s
=> Booting WEBrick
=> Rails 4.1.4 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
...etc...

I like to open a new shell and keep the server running in the background.

Remember to restart the server whenever you make a change that needs initialising – new Gems, config settings etc.

To kick the server use +c and run rails s again. Do this to ensure any configuration changes you made will be recognised by the app through the initialisation process you just restarted.

# Open a new Terminal window, ⌘ + n
$ cd ~/Sites/rails_projects/paymental

From here I can begin coding proper.

Let's start with an anonymous product. This will represent any generic item that my website will sell, it could be anything!

$ rails g scaffold Product name color price_in_pence:integer

I use the scaffold generator for purposes of demonstration and speed. When I'm writing my own projects I always use the more concise Controller and Model generators and write my own code so I know I'm only introducing the code I want.

The scaffold generator takes the same arguments as the Model generator:

  • Model name, (singular version of the resource)
  • Attribute names (columns in the table)
  • Data types (e.g. string, integer, boolean etc.)

I have ommitted the datatype from name, and color because they are strings the generator assumes attributes to be a string by default.

Raking the DB

At this point it's always a good idea to investigate the code that was generated by the generator.

Migrations are single-use templates that are read by rake tasks, for example the migration task: rake db:migrate. Any structural alterations to be made on the database must be done via migration files. You always want to ensure the code in these files is correct before running any rake db task.

$ subl db/migrate/20141015105111_create_products.rb
# db/migrate/20141015105111_create_products.rb
class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.string :color
      t.integer :price_in_pence

      t.timestamps
    end
  end
end

The subl command is an executable that comes with sublime text. Symlink it to a folder in your $PATH variable if you haven't already and restart your shell to open files and folders from the command line with subl <FILE_NAME>. For instructions on how to get that working see here.

After confirming the contents of the migration file are correct, I'm ready to rake the DB.

$ rake db:migrate
== 20141015105111 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0013s
== 20141015105111 CreateProducts: migrated (0.0014s) ==========================

Pointing the browser to http://0.0.0.0:3000/products will show us an empty table of products and a link to create a new one. Go ahead and add in some products ready for us to purchase through Stripe.

Generating Orders

Next up we need an object that represents succesful payment for a product. I'm going to think of this object as an Order. To place an order I'll need some routes, a model, a controller and some views.

I'll start with my routes:

# config/routes.rb
Rails.application.routes.draw do
  root 'products#index'
  resources :products do
    resources :orders
  end
end

This will provide me with the following helper method to link a "Buy" button to a page for placing orders. Also I've defined a "root route" so I no longer have to go to http://0.0.0.0:3000/products because http://0.0.0.0:3000/ will take me to the same place.

# app/views/products/index.html.erb
# ...

<% @products.each do |product" %>

  # ...

    <%= link_to "Buy", new_product_order_path(product), data: { no_turbolink: true } %>

  # ...

<% end %>

# ...

We pass in a data attribute that tells rails not to use turbolinks when we click "Buy". This ensures that our Stripe.js code will work when the page loads after clicking the "Buy" link. For more on turbolinks see here.

We need the product id to be passed in as an argument to the path new_product_order_path helper method, that way the id number will appear in the URL after we click the link. We can use the association to get the price of the product which is necessary for making a charge when an order is placed, e.g. price = @order.product.price_in_pence.

Clicking this link in the browser will cause our app to try and route us (via the rules specified above in config/routes.rb) to a controller that doesn't yet exist. We will see an UninitializedConstant Exception, the missing constant in question is called OrdersController. So let's go ahead and generate that controller now, along with new and create actions and a form at app/views/orders/new.html.erb

$ rails g controller Orders new
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def new
    @product = Product.find(params[:product_id])
    @order = @product.orders.new
  end

  def create
    @product = Product.find(params[:product_id])
    @order = @product.orders.new(order_params)
    if @order.save
      flash[:success] = "Thanks for purchasing #{@product.name}"
      redirect_to product_order_path(@product, @order)
    else
      flash[:error] = "Oops, something went wrong"
      render :new
    end
  end

  private
  def order_params
    params.require(:order).permit(:product_id, :stripe_token)
  end
end

If we try to click through to the new order page now we'll see a NoMethodError. The line with the complaint is trying to reference an Order model. Let's generate that model now and set up the association between a Product and an Order.

$ rails g model Order stripe_token product_id:integer
$ rake db:migrate
== 20141024181050 CreateOrders: migrating =====================================
-- create_table(:orders)
   -> 0.0038s
== 20141024181050 CreateOrders: migrated (0.0039s) ============================

Defining the association below will make a product method available on Order instances.

# app/models/order.rb
class Order > ActiveRecord::Base
  belongs_to :product
end

Likewise defining this association below will make an orders method available to call on Product instances.

# app/models/product.rb
class Product > ActiveRecord::Base
  has_many :orders
end

We're already calling @product.orders.new in the OrdersController so now when we click the link, we'll be routed through to the new view (instead of seeing a NoMethodError), which currently contains nothing more than markup explaining where to find the file.

Custom Stripe Form

Next we need to replace the markup at app/views/orders/new.html.erb with a Stripe form. Stripe's documentation provides a thorough description of the process of accessing their API via ajax through a custom form. First up lets generate the form tag that will send our data back to the create action of our Orders controller using the helper method form_for.

We can pass an array of objects to form_for, which will be used to set the destination of the form data when upon clicking submit.

<%= form_for [@product, @order] do |f| %>

  # ...Stripe form goes here...

<% end %>

We're going to copy the markup from Stripe's documentation: https://stripe.com/docs/tutorials/forms.

<%= form_for [@product, @order] do |f| %>
  <span class="payment-errors"></span>

  <div class="form-row">
    <label>
      <span>Card Number</span>
      <input type="text" size="20" data-stripe="number"/>
    </label>
  </div>

  <div class="form-row">
    <label>
      <span>CVC</span>
      <input type="text" size="4" data-stripe="cvc"/>
    </label>
  </div>

  <div class="form-row">
    <label>
      <span>Expiration (MM/YYYY)</span>
      <input type="text" size="2" data-stripe="exp-month"/>
    </label>
    <span> / </span>
    <input type="text" size="4" data-stripe="exp-year"/>
  </div>

  <button type="submit">Submit Payment</button>
<% end %>

Notice that the none of the input elements that I've copied into the block above have name attributes. The name attribute of an input element is how we name the data being submitted through the form. We can use the name to collect the data on the server side. By omitting the name attributes from this form we can be sure that our servers never see the card credentials of our customers.

Stripe.js

Stripe's javascript code in Stripe.js enables us to communicate with Stripe. We need to include it to reference their functions, which we'll be using to send and receive data to and from their servers.

# app/views/layouts/application.html.erb
<!DOCTYPE html>
  <html>
  <head>
    <title>Paymental</title>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>

    <% if params[:controller] == "orders" and params[:action] == "new" or params[:action] == "create" %>

      <script type="text/javascript" src="https://js.stripe.com/v2/"></script>

    <% end %>

I've included this directly between the <head></head> tags as per Stripe's recommendation but I've included an if statement to ensure Stripe's servers are only contacted on the appropriate pages. Underneath this script tag I can open a new script tag to paste in Stripe's boilerplate js code which will allow me to send the credit card details to Stripe for authentication, and receive a token which represents the verified card details.

# app/views/layouts/application.html.erb
<!DOCTYPE html>
  <html>
  <head>
    <title>Paymental</title>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>

    <% if params[:controller] == "orders" and params[:action] == "new" or params[:action] == "create" %>

      <script type="text/javascript" src="https://js.stripe.com/v2/"></script>
      <script type="text/javascript">
        // This identifies your website in the createToken call below
        Stripe.setPublishableKey('<%= Rails.application.secrets.stripe_public_key %>');
        // ...
      </script>
    <% end %>
  </head>

This setPublishableKey function identifies your site when communicating with Stripe. I've replaced the key (which is available from your Stripe Dashboard upon logging in) with a reference to a configuration variable Rails.application.secrets.stripe_public_key, that I will set in config/secrets.yml.

# config/secrets.yml
development:
  secret_key_base: 1234571uyo2ehbdlwceiug86751836..etc...
  stripe_secret_key: sk_test_vdu32vdp23iy0894h...etc...
  stripe_public_key: pk_test_G124ij9wfmwoim03n...etc...

Setting the test keys in plain text in this file isn't a security risk because charges can't be made via Stripe using these keys. However if you're hosting the code on Github or Bitbucket publicly you may still want to hide the test keys in case someone ends up using them accidently, which might be confusing for you. The live secret key by contrast must always remain hidden from public view under shell variables.

To check the view code is getting the correct key from the secrets file, restart your server and point your browser to http://0.0.0.0:3000/products/1/orders/new (make sure the number in this URL corresponds to a valid product_id. Open up the web inspector and look for the publishable key:

Stripe Publishable Key

Once you've confirmed that the publishable key is available in the browser (pk_test_<HASH>) we can continue following the Stripe Docs and add the JS code that will send the card details to Stripe and retrieve a stripe_token to submit with our form.

# app/views/layoutes/application.html.erb

jQuery(function($) {
  $('#new_order').submit(function(event) { // change $('#payment-form') to $('#new_order')
    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;
  });
});

Make sure to change the jQuery selector that grabs the form to include your form element's correct id attribute value. Simple form automatically provides our form with id="new_order".

The createToken function packages the credit card info into a single-use token that is subsequently sent to Stripe for validation. We get a response object back from Stripe, and want to save the id of this object to our DB for use later when creating a charge for our product when we save an order.

To save the response object's id we use Stripe.js to append an input element with a name attribute to our form before submitting it to our server.

Below we can define the function that handles the response and is passed as a second argument to createToken above.

function stripeResponseHandler(status, response) {
  var $form = $('#new_order'); // change the selector that gets the form to #new_order

  if (response.error) {
    // Show the errors on the form
    $form.find('.payment-errors').text(response.error.message);
    $form.find('button').prop('disabled', false);
  } else {
    // response contains id and card, which contains additional card details
    var token = response.id;
    // Insert the token into the form so it gets submitted to the server
    $form.append($('<input type="hidden" name="order[stripe_token]" />').val(token)); // Change the name attribute to correspond to rails' expected format for the params object.
    // and submit
    $form.get(0).submit();
  }
}

Again, change the jQuery selector from the example given in Stripe's docs. We'll also need to make sure the newly appended input field's name attribute is appropriate for intercepting the data with rails in the traditional way:

# app/controllers/orders_controller.rb
  private
  def order_params
    params.require(:order).permit(:stripe_token)
  end

# The syntax above will only find and permit the "stripe_token" attribute if it's nested inside "order".

# params = {
#   "utf8" => "✓",
#   "authenticity_token" => "sSlwx4lcH8pQYHrT9C5RR50sgbsazaCw16SegVsoiaA=",
#   "order" => {
#     "stripe_token"=>"tok_14pfah2uVktQRyY5UIs2cIhP"
#   },
#   "action" => "create",
#   "controller" => "orders",
#   "product_id" => "1"
# }

# To achieve that we appended "<input name="order[stripe_token]" />" to our form using jQuery.

At this point we're able to verify via Stripe whether someone has a valid form of payment. We can submit test card data and see whether or not we're saving the stripe_token's id in our DB.

Submitting test card details

To see whether or not the data was saved define a show action in your orders controller like so:

# app/controller/orders.rb
def show
  @order = Order.find(params[:id])
  @price = @order.product.price_in_pence.to_f / 100
end

...and create a receipt page:

# app/views/orders/show.html.erb
<article>
  <h1>Thank you</h1>
  <h2><%= @order.product.name %></h2>
  <p>£ <%= @price %></p>
  <p>Stripe token: <%= @order.stripe_token %></p>
</article>

The next step is to use Stripe's Ruby API to create a charge using @order.stripe_token – a reference to the verified card details recognisable to Stripe.


Creating a charge

It's a good idea to have a read through Stripe's Full Api reference before continuing.

At the very least we need to use to Stripe's API to create a charge object and, in so doing, take payment for a product on our site. Let's have a look at Stripe's charge object and find out how to create one. https://stripe.com/docs/api#charges

The object is quite large but thankfully to create one we only need to bundle the Stripe gem, call the createCharge method and pass a few required arguments.

https://stripe.com/docs/api#create_charge

# Gemfile

gem 'stripe', :git => 'https://github.com/stripe/stripe-ruby'
$ bundle install
# app/controllers/orders_controller.rb
def create

    @product = Product.find(params[:product_id])
    @order = @product.orders.new(order_params)

    if @order.save

      Stripe::Charge.create(
        :amount => @product.price_in_pence,
        :currency => "gbp",
        :card => @order.stripe_token # obtained with Stripe.js
    )

      flash[:success] = "Thanks for purchasing #{@product.name}"
      redirect_to product_order_path(@product, @order)

    else

      flash[:error] = "Oops, something went wrong"
      render :new
end

Stripe will return a charge object if the charge succeeds, it will raise an error if it fails.

Let's reload the browser at http://0.0.0.0:3000/products. Create a new product if you haven't already and go back to the index page. Once there click "Buy" to be taken to the new order form and fill out the form with test card details and click "Submit Payment".

You'll see an error complaining that Stripe's API key is not set and there are two things we want to do. Firstly we want to set the API key to silence the complaint and secondly we want to handle errors gracefully, giving information back to the user where appropriate and logging and/or notifying ourselves.

First though, let's confirm everything works the as expected by including a line of code in our orders controller that references the secret key from config/secrets.yml.

# app/controllers/orders_controller.rb
def create

  @product = Product.find(params[:product_id])
  @order = @product.orders.new(order_params)

  if @order.save

    Stripe.api_key = Rails.application.secrets.stripe_secret_key # set the secret key to identify with stripe.

    Stripe::Charge.create(
      :amount => @product.price_in_pence,
      :currency => "gbp",
      :card => @order.stripe_token # obtained with Stripe.js
    )
    flash[:success] = "Thanks for purchasing #{@product.name}"
    redirect_to product_order_path(@product, @order)
  else

    flash[:error] = "Oops, something went wrong"
    render :new
  end
end

If you try making a purchase again you should be redirected to the receipt page where you'll see evidence of the purchase. Log in to your dashboard at Stripe and check that the test payment was successful https://dashboard.stripe.com/test/dashboard.


Now that we're taking payments successfully for products – albeit rather boring and somewhat anonymous products – let's take care of payment failures.

###Error handling

Try making a payment with a test card that will be declined. https://stripe.com/docs/testing#cards

Card Declined

A Stripe::CardError exception will be raised. Let's take a look at Stripe's API documentation again to see how they suggest we handle errors:

https://stripe.com/docs/api#errors

We can rescue errors from our model and do something useful with them:

# app/controllers/orders_controller.rb
def create

  @product = Product.find(params[:product_id])
  @order = @product.orders.new(order_params)

  if @order.save

    Stripe.api_key = Rails.application.secrets.stripe_secret_key # set the secret key to identify with stripe.

    Stripe::Charge.create(
      :amount => @product.price_in_pence,
      :currency => "gbp",
      :card => @order.stripe_token # obtained with Stripe.js
    )
    flash[:success] = "Thanks for purchasing #{@product.name}"
    redirect_to product_order_path(@product, @order)
  else

    flash[:error] = "Oops, something went wrong"
    render :new
  end
rescue Stripe::CardError => e
  body = e.json_body
  err  = body[:error]
  flash[:error] = err[:message]
  render :new
end

The code between rescue and end only executes if the app tries to raise the named exception. So if we type in the details again and the card is declined the error message will be passed to the flash hash and the form will be rendered again.

Let's display the flash messages in the browser with some code in the application layout view:

# app/views/layouts/application.html.erb
<html>
  #...
<body>

  <% flash.each do |key, value| %>
    <div class="<%= key %>">
      <p><%= value %></p>
    </div>
  <% end %>

  <%= yield %>

</body>
</html>

Now if we submit the 4000 0000 0000 0002 card details, we'll be redirected to the form along with an error message.

Heroku Deployment

Once you've downloaded the Herkou toolbelt it's dead easy to Deploy to heroku. The toolbelt gives you a suite of commands to issue on the command line.

$ heroku login
Enter your Heroku credentials.
Email: tim@steer.me
Password:
Could not find an existing public key.
Would you like to generate one? [Yn]
Generating new SSH public key.
Uploading ssh public key /Users/adam/.ssh/id_rsa.pub
$ heroku create
Creating desolate-meadow-9247... done, stack is cedar
http://desolate-meadow-9247.herokuapp.com/ | git@heroku.com:desolate-meadow-9247.gito
Git remote heroku added

$ heroku open

$ heroku open

Settings

There are some gems we need for deploying to Heroku with rails 4.

group :development, :test do
  gem 'sqlite3'
end

group :production do
  gem 'rails_12factor'
  gem 'pg'
end

ruby '2.1.2'
$ bundle install

The rails_12factor Gem handles some configuration necessary for running a Rails app on Heroku. The pg Gem is the Ruby interface to PostgreSQL, which is the database required for a Rails app running on Heroku, so wrap gem 'sqlite3' in a block specifying development and test environments and use gem 'pg' in your production enviroment. Specifying a Ruby version at the bottom of the Gemfile is a good idea to make sure you're using the same version locally and in production and can expect the same behaviour.

Heroku recommend that you use the same database locally and in production. While maintaining parity between your environments in this way is a sensible approach to development, getting PostgreSQL set up and running locally at this point involves many more steps and goes beyond the scope of this tutorial.

$ git add -A .
$ git commit -m 'heroku config'
$ git push origin master
Counting objects: 232, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (198/198), done.
Writing objects: 100% (211/211), 22.31 KiB | 0 bytes/s, done.
Total 211 (delta 102), reused 0 (delta 0)
To git@github.com:tmcdb/paymental.git
   67e173a..83fd83e  master -> master
$ git push heroku master

Reload your app in the browser and notice the error page.

Heroku Error

We can't see the cause of the error in the browser but we can debug it at the command line.

$ heroku logs
...
2014-10-22T17:41:28.158729+00:00 app[web.1]: PG::UndefinedTable: ERROR:  relation "products" does not exist
...

The exception is a PG::UndefinedTable exception. This tells me that I need to rake the database on the server.

Heroku lets us connect to the server and run commands just as we would locally with herkou run.

$ heroku run rake db:migrate
Running `rake db:migrate` attached to terminal... up, run.9289
Migrating to CreateProducts (20141015130457)
== 20141015130457 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0381s
== 20141015130457 CreateProducts: migrated (0.0383s) ==========================

Migrating to CreateOrders (20141015131508)
== 20141015131508 CreateOrders: migrating =====================================
-- create_table(:orders)
   -> 0.0176s
== 20141015131508 CreateOrders: migrated (0.0178s) ============================

Reloading the browser now should give us access to the live app!

Stripe Live Keys

Earlier on we set our Stripe test keys for connecting to Stripe's test API. In order to take real payments on our app we need to identify ourselves with Stripe using our live keys available here.

# config/secrets.yml

# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
  stripe_secret_key: <%= ENV["STRIPE_SECRET_KEY"] %>
  stripe_public_key: <%= ENV["STRIPE_PUBLIC_KEY"] %>

Each key is a reference to an environment variable. We can set them easily on the command line with the Heroku toolbelt.

$ heroku config:set STRIPE_SECRET_KEY=sk_foo STRIPE_PUBLIC_KEY=pk_bar

sk_foo and pk_bar are placeholders for my keys. Paste your keys from Stripe in place of sk_foo and pk_bar. Be careful not to get them mixed up! Remember your public key will be visible in the browser's web inspector, if you set these keys the wrong way round, your secret key will be visible to the world (wide web). If someone gets hold of your secret key you're granting them access to your payment system. If in doubt, reset your API keys.

Alternatively you can set them in the browser by visiting the Heroku dashboard, selecting your app, navigating to the settings tab, clicking "Reveal Config Vars", clicking "Edit", and typing in the new key-value pair at the bottom of the list (it goes without saying that the command line wins every time).

Providing you've activated your Stripe account with your bank account details your app should now be ready to accept real payments.

Hopefully you found this tutorial useful. Let me know your thoughts at tim@steer.me.

To see how to refactor the Charge code into a model or concern to keep the controllers and views cleaner have a look here.

@juanjoseijas
Copy link

Very nice explanation. Works like a charm!
I was wondering what do you think is the best way to add simple customer data in this workflow (name, email, address).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment