Skip to content

Instantly share code, notes, and snippets.

@tmcdb
Last active February 21, 2017 21:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tmcdb/1e3891c83969e197f00b to your computer and use it in GitHub Desktop.
Save tmcdb/1e3891c83969e197f00b to your computer and use it in GitHub Desktop.

Integrating Stripe with Rails (Concise)

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.

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


New project

$ 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

###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.)

Raking the DB

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.

Generating Orders

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 %>

# ...

More on turbolinks.

###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).

Custom Stripe Form

###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.

Stripe.js

###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

Stripe 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.

Submitting test card details

###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

Create a charge

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.

Submitting test card details

###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

Card Declined

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

Card Declined

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.

Heroku Deployment

###Download the Herkou toolbelt Deploy to heroku.

$ 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

###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 git@github.com:tmcdb/paymental.git
   67e173a..83fd83e  master -> master
$ git push heroku master

###Reload the browser Notice the error.

Heroku 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!

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

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.

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