DEV Community

Alessandro Rodi
Alessandro Rodi

Posted on • Edited on

Integrate the new Stripe Checkout in Ruby on Rails.

Are you ready for Strong Customer Authentication?

Since April 2019, Stripe provides a new Checkout service. Let's see how to integrate it step by step. I'll show you how to integrate the new Stripe Checkout service into your Rails application. This service allows us to seamlessly integrate a Stripe Checkout form, conform to the new Strong Customer Authentication EU regulation.

As always, the Stripe documentation is great, but it took me a bit to understand what was the right approach for my situation. You can re-use this tutorial in any new Rails Application that sells a product. I'll not go into implementation details, but I'll simply suppose you know Rails and you know how to run a migration and manage your models. I'll just cover the parts to connect your system with Stripe.

Basic setup

Create Stripe Account and Product

Please refer to the good Stripe documentation to create an account and a product, your clients can subscribe to. You should end up with something like this:
An example product with two plans: professional and enterprise
An example product with two plans: professional and enterprise.

User and Subscription

These are the two models we will use in our system. They must have the following fields:

create_table "users" do |t|
    t.string "email", null: false
    t.string "stripe_id"
end

create_table "subscriptions" do |t|
    t.string "plan_id"
    t.integer "user_id"
    t.boolean "active", default: true
    t.datetime "current_period_ends_at"
    t.string "stripe_id"
end
Enter fullscreen mode Exit fullscreen mode

Both have a reference to their Stripe counterpart and a User has_one Subscription.

Proceed to Checkout

When a customer subscribes to a plan, a Subscription gets created. Since we need to associate the Subscription to an existing User, we have to use the client-server integration, where the Checkout Session is created server-side.
Let's start by creating the controller:

class Stripe::CheckoutsController < ApplicationController
  def new
    session = Stripe::Checkout::Session.create(
        payment_method_types: ['card'],
        subscription_data: {
            items: [{ plan: params[:plan] }],
        },
        customer: current_user.stripe_id,
        client_reference_id: current_user.id,
        success_url: create_checkout_url(session_id: '{CHECKOUT_SESSION_ID}'),
        cancel_url: root_url,
    )

    render json: { session_id: session.id }
  end
end
Enter fullscreen mode Exit fullscreen mode

and add the routes:

namespace :stripe do
  resources :checkouts
  post 'checkout/webhook', to: "checkouts#webhook"
end

resources :subscriptions
Enter fullscreen mode Exit fullscreen mode

This controller initialises a Checkout Session for a given plan and defines the two URLs that will be invoked for a successful subscription or a failed one. In case of success, we go on the create action, otherwise we simply go to the root url. You can customise that later.

For now, we will focus on returning a JSON with the session_id that we need.

The second step is to create a subscribe button on our pricing page. Please take inspiration by this simple Javascript example.

Given this button:

<a data-subscribe="professional" href="#">Sign Up</a>
Enter fullscreen mode Exit fullscreen mode

we can define this Javascript to implement a checkout:

document
  .querySelector('[data-subscribe]')
  .addEventListener('click', (event) => {
    fetch(`/subscriptions/new?plan=${event.currentTarget.dataset.subscribe}`)
    .then(response => response.json())
    .then((json) => {
      var stripe = Stripe('<YOUR_STRIPE_PUBLIC_KEY');
      stripe.redirectToCheckout({
        sessionId: json.session_id
      })
    .then(function (result) {
    });
  });
  event.returnValue = false;
});
Enter fullscreen mode Exit fullscreen mode

once clicked, the button starts a request to the server to generate a session for the selected plan. The session id is then returned to the browser that redirects to the checkout window offered by Stripe.

Configuring a webhook

We cannot just rely on a call to the success_url we defined above. The user might close the browser before this page is called or the connection might drop, leaving you with a paying customer without an account. In order to manage this case, we will integrate a Webhook, that we are sure will be called, and that will manage the correct user registration. 

Create a webhook on Stripe

You can create a webhook for the checkout event from the Stripe Dashboard or by using APIs. Our Webhook will be triggered for a checkout.session.completed event and will perform a call to https://yourapp.com/stripe/checkout/webhook. Remember to add this webhook to both your test and live environment in Stripe.

Create a controller action

For this example, we will keep it simple, and imagine that our User is already logged in when subscribing. Your controller action will look like:

def webhook
  sig_header = request.env['HTTP_STRIPE_SIGNATURE']

  begin
    event = Stripe::Webhook.construct_event(request.body.read, sig_header, ENV['STRIPE_ENDPOINT_SECRET'])
  rescue JSON::ParserError
    return head :bad_request
  rescue Stripe::SignatureVerificationError
    return head :bad_request
  end

  webhook_checkout_session_completed(event) if event['type'] == 'checkout.session.completed'

  head :ok
end

private 

def build_subscription(stripe_subscription)
    Subscription.new(plan_id: stripe_subscription.plan.id,
                     stripe_id: stripe_subscription.id,
                     current_period_ends_at: Time.zone.at(stripe_subscription.current_period_end))
end

def webhook_checkout_session_completed(event)
  object = event['data']['object']
  customer = Stripe::Customer.retrieve(object['customer'])
  stripe_subscription = Stripe::Subscription.retrieve(object['subscription'])
  subscription = build_subscription(stripe_subscription)
  user = User.find_by(id: object['client_reference_id'])
  user.subscription.interrupt if user.subscription.present?
  user.update!(stripe_id: customer.id, subscription: subscription)
end
Enter fullscreen mode Exit fullscreen mode

Now, you can install the Stripe CLI and run the following command, that will forward the webhooks calls to your local environment.

stripe listen - forward-to localhost:3000/stripe/checkout/webhook
Enter fullscreen mode Exit fullscreen mode

This command will intercept the webhooks and print a webhook signing secret that you should set as STRIPE_ENDPOINT_SECRET env variable and restart the server.

Success endpoint

When the user finishes the payment process, will be redirected to the success_url. In this create action we just set a flash message and redirect to the root_url

# stripe/checkouts_controller.rb
def create
  flash[:success] = "You subscribed to our plan!"
  redirect_to root_path
end
Enter fullscreen mode Exit fullscreen mode

Customizing the checkout form

Stripe gives you the possibility to customize the new Checkout form with a certain colours and a logo. You can proceed in your Branding Settings to start customizing the form.

Upgrade the plan

The procedure you just implemented can be re-used to Upgrade the plan to a different one. The Stripe Session Checkout will take care of it for you.

Interrupt a subscription

Your controller should implement the following:

# subscriptions_controller.rb
def interrupt
  current_user.subscription.interrupt
end

# models/subscription.rb
def interrupt
  Stripe::Subscription.delete(stripe_id)
  self.active = false
  save
end
Enter fullscreen mode Exit fullscreen mode

Invoices

Recurring payments and invoice and entirely managed by Stripe. You can supply a link to your customers to download the invoices through something like this:

Stripe::Invoice.list(limit: 3, customer: stripe_id).first.invoice_pdf
Enter fullscreen mode Exit fullscreen mode

Edit payment information

Stripe takes care of many notifications to your customers for you. When the customer credit card is expiring or is already expired you should allow them to edit their card details. Following the first example we need an action that looks like the following:

def edit
  session = Stripe::Checkout::Session.create(
    payment_method_types: ['card'],
    mode: 'setup',
    setup_intent_data: {
      metadata: {
        customer_id: current_user.stripe_id,
         subscription_id: current_user.subscription.stripe_id,
      },
    },
    customer_email: current_user.email,
    success_url: CGI.unescape(subscription_url(session_id: '{CHECKOUT_SESSION_ID}')),
    cancel_url: subscription_url
  )

  render json: { session_id: session.id }
end
Enter fullscreen mode Exit fullscreen mode

and a button that, once clicked, executes the following Javascript code:

fetch('/checkout/edit')
      .then(response => response.json())
      .then((json) => {
        Stripe(YOUR_STRIPE_ID).redirectToCheckout({sessionId: json.session_id})
          .then(function (result) {
          });
      });
Enter fullscreen mode Exit fullscreen mode

Recurring payment webhook

Every time the subscription is renewed, and Stripe charges the customer, you want to be notified in order to keep your customer subscription active. We will approach this by implementing a scheduled task that will run every night and check expiring subscriptions.

Top comments (9)

Collapse
 
cecilerx profile image
Cécile Rougnaux

I have a question for my models:
Should I set them like :

class user 
has_many :subscriptions 

class subscription
belongs_to: user
Enter fullscreen mode Exit fullscreen mode

and in this case, when I write the migration to create the subscriptions table:

user_id:references
Enter fullscreen mode Exit fullscreen mode

Thanks for your help!

Collapse
 
yverbytskyi profile image
Yurii Verbytskyi

Nice post, thanks a lot. But no routes posted after and add the routes: :)

Collapse
 
emmetgibney profile image
Emmet Gibney

Hi Alessandro, could you update with the routes as well? Thanks!

Collapse
 
matthewjberg profile image
Matt Berg

This is a great post, thank you for putting this together. How does this change for a product instead of a subscription?

Collapse
 
coorasse profile image
Alessandro Rodi

Not so different, check the Stripe documentation here: stripe.com/docs/api/checkout/sessi...

Collapse
 
ovsjah profile image
Ovsjah Schweinefresser

Thanks, щастья - здоровья!

Collapse
 
anmolboparai profile image
anmolboparai

this is the routes you need

namespace :stripe do
resources :checkouts
post 'checkout/webhook', to: "checkouts#webhook"
end

resources :subscriptions

Collapse
 
shilpi230 profile image
Shilpi Agrawal

How is checkouts/new method is being called ?

Collapse
 
peterfealey profile image
Pete Fealey

Hey! Thanks for the great tutorial. Any chance you can post the Routes as they're missing.