Building an Online Shop with STRIPE Integration Ridiculously Fast
Do you want to get a Ecomm shopping cart up and running super quick? Perhaps you just don’t want the extra costs of Shopify or other platforms? Or maybe you just want to play with payment systems? In this quick blog I’ll take you through every step to get a Online Shop up and running with a close to zero cost. Quickly.
The data flow is as follows:
Product Page — Checkout Summary — Payment Form — STRIPE — Order Page
This article describes how to code the whole of the data flow described above into Production ready environment using freely available coding frameworks, libraries and interfaces.
Setup
Getting your development environment setup isn’t something I’m going to dive into during this piece. What you should do is get the following all working in your dev environment:
Rails 6 (I used Rails 6.0.3.7)
Ruby 2.6 (I used ruby 2.6.6p146)
The rest is just rails GEMs (libraries) which I’ll show you how to install as we go along.
Fasten Your Seat Belt
The beauty of using using Ruby on Rails is we can create a new application, connect it to a database, bundle in essential JS files using Webpack, and libraries easily. The template used in this case first injects the necessary GEMs and stylesheet assets. Then the routes, app controller, devise authentication and environment variables are set. We’re going to build a carpet shop called “Delcalifa” which sells three different carpets. Begin by starting arails app as follows:
rails new \
--database postgresql \
--webpack \
-m https://raw.githubusercontent.com/haseebc/rails-templates/master/devise.rb \
delcalifa
Okay now we start the heavy lifting of working on the database schema, application routes and front end views. We need to do this for the Product Page, Stripe integration and then the Order summary. Vamos, lets begin.
Product Page
The first thing we want to do is get the product page setup.
Model
A model maintains the relationship between the objects and the database and handles validation, association, transactions, and more.
This subsystem is implemented in ActiveRecord library, which provides an interface and binding between the tables in a relational database and the Ruby program code that manipulates database records. Ruby method names are automatically generated from the field names of database tables.
rails g model Category name:string
rails g model Carpet sku:string name:string category:references photo_url:string
Then create the fileseeds.rb
which resides in the db directory.
puts 'Cleaning database...'
Carpet.destroy_all
Category.destroy_allputs 'Creating categories...'
antique = Category.create!(name: 'antique')
modern = Category.create!(name: 'modern')puts 'Creating carpets...'
Carpet.create!(sku: 'original-carpet', name: 'Traditional carpet', category: antique, photo_url: 'http://onehdwallpaper.com/wp-content/uploads/2015/07/Teddy-Bears-HD-Images.jpg')Carpet.create!(sku: 'modern-turkish', name: 'Jean-Michel - Le Wagon', category: modern, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Carpet.create!(sku: 'modern-iranian', name: 'Octocat - GitHub', category: modern, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'
Populate the db by rails seeds.rb
.
Controller
This is the facility within the application that directs traffic, on the one hand, querying the models for specific data, and on the other hand, organizing that data (searching, sorting, messaging it) into a form that fits the needs of a given view.
This subsystem is implemented in ActionController, which is a data broker sitting between ActiveRecord (the database interface) and ActionView (the presentation engine).
rails g controller carpets
Routes
Routes are rules written in a Ruby DSL (Domain-Specific Language). We edit the routes.rb
file.
devise_for :users
root to: 'carpets#index'
resources :carpets, only: [:index, :show]
Update the controller for carpets
# app/controllers/carpets_controller.rb
class CarpetsController < ApplicationController
skip_before_action :authenticate_user!def index
@carpets = Carpet.all
enddef show
@carpet = Carpet.find(params[:id])
endend
Views
carpets/index.html.erb<div class="container">
<h1>Carpets</h1>
<div class="row">
<% @carpets.each do |carpet| %>
<div class="col-6 col-md-4">
<div class="card">
<%= image_tag carpet.photo_url , class: "card-img-top" %>
<div class="card-body">
<h5 class="card-title"><%= carpet.name %></h5>
<p class="card-text"><%= carpet.category.name %></p>
<%= link_to "More details", carpet_path(carpet), class: "btn btn-primary" %>
</div>
</div>
</div>
<% end %>
</div>
</div>carpets/show.html.erb
<div class="container py-3">
<div class="row">
<div class="col-sm-12 col-md-6">
<%= image_tag(@carpet.photo_url, width: '100%') %>
</div>
<div class="col-sm-12 col-md-6">
<h1><%= @carpet.name %></h1>
<p>Some awesome description of our amazing carpets. Cool</p>
</div>
</div>
</div>
Now we need to set the default currency, we begin by adding the necessary GEM.
# Gemfile
gem 'money-rails'
bundle
rails g money_rails:initializer
Edit the newly created ruby file, in this case we go for euros.
# config/initializers/money.rb
# https://github.com/RubyMoney/money#deprecation
Money.locale_backend = :currency
MoneyRails.configure do |config|
config.default_currency = :eur # or :gbp, :usd, etc.
# [...]
end
Run a migration
rails db:migrateEnable monetize
# app/models/carpet.rb
class Carpet < ApplicationRecord
belongs_to :category
monetize :price_cents
end
DATABASE
Don’t forget to set price in your seeds that you created earlier! Updated file below:
puts 'Cleaning database...'
Carpet.destroy_all
Category.destroy_allputs 'Creating categories...'
antique = Category.create!(name: 'antique')
modern = Category.create!(name: 'modern')puts 'Creating carpets...'
Carpet.create!(price_cents: 950, sku: 'original-carpet', name: 'Traditional carpet', category: antique, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')Carpet.create!(price_cents: 150, sku: 'modern-turkish', name: 'Jean-Michel - Le Wagon', category: modern, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Carpet.create!(price_cents: 1000, sku: 'modern-iranian', name: 'Octocat - GitHub', category: modern, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'
Run the seeding and the migration
rails db:seed
rails db:migrate
bundle install
Now point to http://localhost:3000/carpets just to make sure all is cool, price should be in cents as part of the euro currency.
Okay so we now have products with an associated price. Now the fun starts.
STRIPE Integration
Make sure you setup a new Stripe account and then add the Stripe GEM.
# Gemfile
gem 'stripe'
bundle install
Next configure the initialiser ensuring you add the API keys to the .env file. Be super sure not to upload the keys to Github by updating .gitignore
.
#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]
Make an Order
Update the model.
rails g model Order state:string carpet_sku:string amount:monetize checkout_session_id:string user:references carpet:references
Ensure you edit the migration to remove the currency.
t.monetize :amount, currency: { present: false }
The migration file should look like this:
class CreateOrders < ActiveRecord::Migration[6.0]
def change
create_table :orders do |t|
t.string :state
t.string :carpet_sku
t.string :checkout_session_id
t.references :user, null: false, foreign_key: true
t.references :carpet, null: false, foreign_key: true
t.monetize :amount, currency: { present: false }
t.timestamps
end
end
end
Then after running the migrationdb:migrate
enable montize in order.rb
.
# app/models/order.rb
monetize :amount_cents
Controller
rails g controller orders
Routes
# config/routes.rb
resources :orders, only: [:show, :create]
Views
Here’s we’re going to edit the carpet page and add a form to create an order.
# app/views/carpets/show.html.erb
<%= form_tag orders_path do %>
<%= hidden_field_tag 'teddy_id', @teddy.id %>
<%= submit_tag 'Purchase', class: 'btn btn-primary' %>
<% end %>
Controllers
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
carpet = Carpet.find(params[:carpet_id])
order = Order.create!(carpet: carpet, carpet_sku: carpet.sku, amount: carpet.price, state: 'pending', user: current_user)
session = Stripe::Checkout::Session.create(
payment_method_types: ['card'],
line_items: [{
name: carpet.sku,
images: [carpet.photo_url],
amount: carpet.price_cents,
currency: 'eur',
quantity: 1
}],
success_url: order_url(order),
cancel_url: order_url(order)
)
order.update(checkout_session_id: session.id)
redirect_to new_order_payment_path(order)
end
end
Accepting Payments
Here we make a routes and controller modification. Then change the view for the payment form.
Routes
# config/routes.rb
resources :orders, only: [:show, :create] do
resources :payments, only: :new
end
Controller
Edit Payment controller
class PaymentsController < ApplicationController
def new
@order = current_user.orders.where(state: 'pending').find(params[:order_id])
end
end
Views
<!-- app/views/payments/new.html.erb -->
<div class="container">
<div class="row py-4">
<div class="col-md-3"><%= image_tag @order.carpet.photo_url, width: '100%' %></div>
<div class="col">
<h1>Checkout summary</h1>
<p>Purchase of <strong><%= @order.carpet.name %></strong> for <strong><%= humanized_money_with_symbol @order.amount %></strong></p>
<button id="pay" class="btn btn-primary">Pay</button>
<!-- # Commented for the lecture, remove comments. -->
<script src="https://js.stripe.com/v3/"></script>
<script>
const paymentButton = document.getElementById('pay');
paymentButton.addEventListener('click', () => {
const stripe = Stripe('<%= ENV['STRIPE_PUBLISHABLE_KEY'] %>');
stripe.redirectToCheckout({
sessionId: '<%= @order.checkout_session_id %>'
});
});
</script>
</div>
</div>
</div>
Controller
Edit the Orders controller
def show
@order = current_user.orders.find(params[:id])
end
Views
<div class="container">
<div class="row py-4">
<div class="col-md-3"><%= image_tag @order.carpet.photo_url, width: '100%' %></div>
<div class="col">
<h1>Order #<%= @order.id %> <small><span class="badge badge-primary"><%= @order.state %></span></small></h1>
<ul class="list-unstyled">
<li><strong>ISSUED ON</strong> <%= @order.created_at.to_date %></li>
<li><strong>CARPET</strong> <%= @order.carpet.name %> [<%= @order.carpet_sku %>]</li>
<li><strong>TOTAL</strong> <%= humanized_money_with_symbol @order.amount %></li>
</li>
</ul>
<p>Estimated delivery in <strong>2 business days</strong></p>
</div>
</div>
</div>
Updating the Order
Get Github setup
Create new repository called delcalifa_stripe.
git remote add origin git@github.com:haseebc/delcalifa_stripe.git
git push -u origin master
Production Hosting
In this case we utilise heroku for the hosting environment. Set yourself a fee account and then get going with creating the app space as follows:
heroku create delcalifanew --region eu
heroku git:remote -a delcalifanew
So how do we update the order?
Right now, the order is still pending 🤔
Lets use Stripe webhooks!
STRIPE Webhooks
Begin by adding the associated GEM file.
# Gemfile
gem 'stripe_event'
Make a route change
# config/routes.rb
mount StripeEvent::Engine, at: '/stripe-webhooks'
Webhook configuration
Go on your Stripe dashboard to create a webhook
https://dashboard.stripe.com/test/webhooks
And add your webhook
URL endpoint: <HOST URL>/stripe-webhooks
Go to the test data part of the Stripe dashboard and add this webhook
https://delcalifanew.herokuapp.com/stripe-webhooks
Use the previous Stripe publishable key and secret key that we obtained.
Webhook signing secret is specific to the endpoint.
What you will have now is two sets of important API keys. One for Test and One for Prod. Its super important to understand that Test here is all the API keys you will add to Heroku when you want to run tests. These API keys are also stored and available in the Stripe dashboard.
For example for Test
STRIPE_PUBLISHABLE_KEY
pk_test_something
STRIPE_SECRET_KEY
sk_test_something
Webhook signing secret
whsec_something
For Production
STRIPE_PUBLISHABLE_KEY
pk_live_something
STRIPE_SECRET_KEY
sk_live_something
Webhook signing secret
whsec_something
Test locally first
- Install ngrok
- /ngrok http 3000 — this will give a hook into https for rails on 3000
- Copy the url into Stripe webhooks
for example https://8d57fb16f8ea.ngrok.io/stripe-webhooks
Now run a test but dont try and run checkout session completed. Run charge succeeded or something.
Ensure in environments development this line is there
config.hosts.clear - You’ll get a 200 in the ngrok logs
Test in Heroku
- Make sure webhook for test and prod is setup
For example for test data https://delcalifanew.herokuapp.com/stripe-webhooks
And for Pro data. - In Heroku you will use test or prod webooks and STRIPE keys. Depends on what you want. I use test all the time as my shop is not live. Going live is just a cash of using the Prod API keys.
Credit card details to test with are here for Stripe:
# Standard credit card
4242 4242 4242 4242
# Credit card with 3D Secure 2
4000 0000 0000 3220
There are a lot more Stripe card details here.
Useful Links
Stripe have done a really good job on their documentation. Some links here.
https://stripe.com/docs/legacy-checkout/rails
https://stripe.com/docs/payments/checkout
Below is some further useful info on setting up a rails cart without the payment system integration.
https://github.com/howardmann/Tutorials/blob/master/Rails_Shopping_Cart.md
Security
Securing the Online Shop is relatively straightforward. You want to think about all the ways data moves between interfaces and then apply security to this (input validation, access and session management), I’ll aim to write this up soon.
However, when working with API keys make sure you make your Github repository private, with Heroku apply secondary authentication to your access and for Strip dashboard access also use secondary authentication. Mishandling the API keys can lead to lots of bad stuff quickly!
If you have any questions please feel free to contact me. When I’m not riding or traveling I build stuff at El Born Lab.