This written tutorial is a concise version of this tutorial, which 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.
- 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
$ rails new paymental
$ cd paymental
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
###Run the app 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...
###Open a new shell To 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.
# Open a new Terminal window, ⌘ + n
$ cd ~/Sites/rails_projects/paymental
##Code
We need a model for Products we're selling:
$ rails g scaffold Product name color price_in_pence:integer
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.)
Check the code that was generated by the generator.
# 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
###Rake the DB. After confirming the contents of the migration file are correct.
$ rake db:migrate
== 20141015105111 CreateProducts: migrating ===================================
-- create_table(:products)
-> 0.0013s
== 20141015105111 CreateProducts: migrated (0.0014s) ==========================
###Check the browser
Add in some products ready purchasing through Stripe http://0.0.0.0:3000/products.
To place an order I'll need a model called Order
, a controller and some views, and routes (URLs).
#####Routes:
# config/routes.rb
Rails.application.routes.draw do
root 'products#index'
resources :products do
resources :orders
end
end
###Link to the orders/new.html.erb
page.
Use no_turbolink: true
option so the page actually loads after clicking (in stead of using turbolinks).
# app/views/products/index.html.erb
# ...
<% @products.each do |product" %>
# ...
<%= link_to "Buy", new_product_order_path(product), data: { no_turbolink: true } %>
# ...
<% end %>
# ...
###Try the link
And see an UninitializedConstant
Exception.
###Generate the controller
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
###Try the link again
And see a NoMethodError
.
###Generate Order
model
$ 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) ============================
###Associate models with eachother
Make a product
method available to call on Order
instances:
# app/models/order.rb
class Order > ActiveRecord::Base
belongs_to :product
end
Make an orders
method available to call on Product
instances.
# app/models/product.rb
class Product > ActiveRecord::Base
has_many :orders
end
###Try the link a third time
Find nothing more than markup explaining where to find the file (not a NoMethodError
as before).
###Replace the markup at app/views/orders/new.html.erb
with form_for
method.
<%= form_for [@product, @order] do |f| %>
# ...Stripe form goes here...
<% end %>
###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 %>
Note 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.
###Include Stripe.js For communicating with Stripe's 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 %>
###Set publishable key For identifying your site with Stripe.
# 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>
###Save secret and public keys
From Stripe Dashboard copy them to 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.
###Restart the server and point check browser Make sure the number in this URL http://0.0.0.0:3000/products/1/orders/new corresponds to a valid product_id
###Use web inspector to verify the publishable key
###Retrieve a stripe_token
to submit with form.
Use the form element's correct id
attribute value (form_for
method in the view code automatically provides id="new_order"
).
# 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;
});
});
###Save the response object's id
Handle the Stripe response and append an input element with a name
attribute to the form before submitting.
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();
}
}
###Permit the stripe_token param
It comes from the <input name='order[stripe_token]' >
element appended by the code above.
# app/controllers/orders_controller.rb
private
def order_params
params.require(:order).permit(:stripe_token)
end
###Submit test card data
See whether or not the stripe_token
's id
is saved to the DB.
###Define a show
action in the orders controller
# app/controller/orders.rb
def show
@order = Order.find(params[:id])
@price = @order.product.price_in_pence.to_f / 100
end
###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>
#Stripe's Ruby API
Read through Stripe's Full Api reference before continuing https://stripe.com/docs/api#charges.
###Bundle the Stripe Gem
# Gemfile
gem 'stripe', :git => 'https://github.com/stripe/stripe-ruby'
$ bundle install
###Call the createCharge
method
https://stripe.com/docs/api#create_charge
# 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.
###Set the API key to silence the complaint.
# 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
###Try making a purchase again Find evidence of the purchase upon redirection.
###Check that the test payment was successful Log in to Stripe Dashboard.
###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.
###Rescue Errors Do something useful with them https://stripe.com/docs/api#errors
# 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.
###Submit the details again
The card is declined the error message will be passed to the flash hash and the form will be rendered again.
###Display the flash messages
# app/views/layouts/application.html.erb
<html>
#...
<body>
<% flash.each do |key, value| %>
<div class="<%= key %>">
<p><%= value %></p>
</div>
<% end %>
<%= yield %>
</body>
</html>
###Submit the 4000 0000 0000 0002
card details yet again
And be redirected to the form along with an error message.
###Download the Herkou toolbelt Deploy to heroku.
$ 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
###Setup Gems for deployment
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'rails_12factor'
gem 'pg'
end
ruby '2.1.2'
$ bundle install
For info on dev/prod parity see here.
###Commit and deploy
$ 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 the browser Notice the error.
###Debug the error 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.
###Rake the database on the server.
$ 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) ============================
###Reload the browser See 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
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.