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.
- Setting up a basic Rails app with a scaffold generator
- Integrating Stripe charges
- Deploying to Heroku
- Ruby 1.9.3 or higher – http://rvm.io/
- Rails 4 – http://rubyonrails.org/
- Postgresql – http://brew.sh/
- Heroku toolbelt – http://toolbelt.heroku.com/
- Git - http://git-scm.com/
- An IDE – http://www.sublimetext.com/
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.
- New Project
- Raking the DB
- Generating Orders
- Custom Stripe Form
- Stripe.js
- Error Handling
- Deployment to Heroku
- Stripe Live Keys
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.
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 [email protected]: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 runrails 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.
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 withsubl <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.
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.
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'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:
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.
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.
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
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.
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: [email protected]
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/ | [email protected]:desolate-meadow-9247.gito
Git remote heroku added
$ heroku open
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 [email protected]:tmcdb/paymental.git
67e173a..83fd83e master -> master
$ git push heroku master
Reload your app in the browser and notice the error page.
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!
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
andpk_bar
are placeholders for my keys. Paste your keys from Stripe in place ofsk_foo
andpk_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 [email protected].
To see how to refactor the Charge code into a model or concern to keep the controllers and views cleaner have a look here.
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).