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.
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
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
and add the routes:
namespace :stripe do
resources :checkouts
post 'checkout/webhook', to: "checkouts#webhook"
end
resources :subscriptions
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>
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;
});
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
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
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
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
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
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
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) {
});
});
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)
I have a question for my models:
Should I set them like :
and in this case, when I write the migration to create the subscriptions table:
Thanks for your help!
Nice post, thanks a lot. But no routes posted after
and add the routes:
:)Hi Alessandro, could you update with the routes as well? Thanks!
This is a great post, thank you for putting this together. How does this change for a product instead of a subscription?
Not so different, check the Stripe documentation here: stripe.com/docs/api/checkout/sessi...
Thanks, щастья - здоровья!
this is the routes you need
namespace :stripe do
resources :checkouts
post 'checkout/webhook', to: "checkouts#webhook"
end
resources :subscriptions
How is checkouts/new method is being called ?
Hey! Thanks for the great tutorial. Any chance you can post the Routes as they're missing.