February 7, 2022
•Last updated November 5, 2023
Stripe Checkout and Stripe Customer Portal Ruby on Rails Tutorial
Accepting payments has become easier than ever. Stripe is a platform that enables so many entrepreneurs to branch out and make their way to building products, tools, and services that can be paid for digitally and in many varied formats.
While Stripe Checkout is simple at face, there are a few important steps to take when adding it to a Ruby on Rails app for maximum security and compatibility. This is a concise guide on how to add Stripe Checkout and Stripe Customer Portal to your Ruby on Rails app. These two offerings complete the circle of a complete billing solution that's nearly drop-in.
A summary of what's involved here:
Stripe Checkout - Offer subscriptions or fixed-price products for sale in a convenient and secure checkout UI.
Stripe Customer Portal - Give your customers a place to easily manage their current subscription(s).
Goals
For this guide, I'll be building a small premium content-based app for purely demonstration purposes. To get access to more content you would pay a subscription fee in a recurring fashion. One could argue that Ruby on Rails is overkill for this concept but think of it as room to grow π.
A Preface
In this guide, I use an application template called kickoff_tailwind. It saves me time when doing these types of tutorials though often the defaults it provides are overkill. If you look under the surface in this guide you'll notice another gem called pay-rails is in use.
We won't be leveraging that gem for this tutorial but I wanted to mention it to avoid any confusion since it comes pre-installed within the template. The gem provides more of a customized approach towards billing in a given rails app. If you need more control that'd be a great avenue to explore.
My goal with this tutorial is to use built-in tools straight from Stripe that allow you to get billing done in less than a day.
Start with Stripe
To get started you will need a Stripe account. In a "testing" state you won't need to have your account activated. Eventually, to accept real payments, you will need to activate your account (provide your billing information).
Head to stripe.com to create an account and circle back here.
API keys
Stripe's API is one of the best out there. It's versioned based on your account so you can upgrade to a new version whenever you decide. Accessing the API is possible through a set of API keys issued when your account gets created.
As of right now, Stripe issues a private and public key. Your public key can be public, (as if that wasn't already obvious) but your private key should be treated as a secure password. Be sure to never commit your private key to version control unless it's encrypted (Encryption is something modern versions of Ruby on Rails helps with and that we will utilize coming up).
Once you find your API keys we can kick off the app build β¨.
Starting the project
We'll make use of Rails 7 (gem install rails
should get you the latest version. You'll need Ruby version 2.7.0 or newer.) for this tutorial. With Rails 7 comes some new defaults which are no longer reliant on Webpack. Read more about the shift here.
Generate a new app:
$ rails new stripe_checkout_portal_tutorial -m kickoff_tailwind/template.rb
This command creates a new app and locally references an application template I created to help speed up some default configurations I tend to use. You're free to skip but there might be some discrepancies in your results as you follow along here.
I cloned that application into my current workspace. Assuming you have a similar path you can copy and paste the code above. You may need to supply something more elaborate.
From here I'll cd
into stripe_checkout_portal_tutorial
and run bin/dev
.
If this doesn't work for you, it probably means you don't have the foreman gem installed. Be sure to run gem install foreman
before continuing.
This should fire up two processes which you can find in Procfile.dev
:
- The infamous Rails server
- Tailwind CSS
Navigating to localhost:3000
should render my template's index page if you're making use of it. If not you should see the default rails hello world page.
Configuring your application to use Stripe
Stripe has both a front-end and back-end (server-side) responsibility. We will need to leverage the Stripe Ruby gem to make API calls from the server-side portion of the app. We also need to use the Checkout API to display things correctly on the front-end. Let's get started.
Install the Ruby gem
If you're not using my template be sure to add the line below to the Gemfile
inside your app making sure to keep it outside any development or test groups. However, if you are using the template, consider this step complete.
# Gemfile
gem 'stripe'
Then run:
bundle install
Stripe CLI
A newish useful tool from Stripe is their CLI. This is great for listening for events locally on your machine as opposed to the old way of having to trigger test events from your Stripe account. We'll make more use of this in a bit but for now, we need to install the CLI.
I'm on a mac and use homebrew.
brew install stripe/stripe-cli/stripe
Adding a checkout button to a view
With the Stripe gem installed we can proceed to add a button to a given view that will be responsible for launching the Stripe Checkout experience.
For most products and services offered online what you sell is typically one-off or subscription-based. Stripe Checkout makes this super quick to get going with but is often limited if you need to accept variable-style amounts (i.e. pay what you want).
For more control, you could swap Stripe Checkout for Stripe Elements instead but I've found checkout is pretty flexible in most cases.
Create a pricing page
Let's create a basic pricing page and add a single subscription-based product to it.
I'll first create the rails controller and basic action.
rails g controller static pricing --skip-routes --skip-stylesheets
This command will generate a few files. I passed a couple of flags to skip the default routing and stylesheets the generator normally generates.
create app/controllers/static_controller.rb
invoke erb
create app/views/static
create app/views/static/pricing.html.erb
invoke test_unit
create test/controllers/static_controller_test.rb
invoke helper
create app/helpers/static_helper.rb
invoke test_unit
I'll add some enhanced routing to denote how the pricing page should render.
# config/routes.rb
require 'sidekiq/web'
Rails.application.routes.draw do
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
devise_for :users
# Add 3 these lines below
scope controller: :static do
get :pricing
end
root to: 'home#index'
end
This is the state of my current config/routes.rb
file. It has some extra fluff from the template as previously stated. You don't need to be too concerned about what's going on here. If you are new to routing, check out this guide I created for a crash course on the basics.
Add a link to pricing in the main navigation
I want to add a link to the pricing page we just generated which takes the place of the "Basic Link" found in the template.
Swap this:
<!-- app/views/shared/_navbar.html.erb-->
<%= link_to "Basic Link", "#", class: "p-3 hover:bg-gray-50 transition ease
duration-300 rounded-lg text-center focus:bg-gray-100 lg:inline-block block" %>
For this:
<!-- app/views/shared/_navbar.html.erb-->
<%= link_to "Pricing", pricing_path, class: "p-3 hover:bg-gray-50 transition
ease duration-300 rounded-lg text-center focus:bg-gray-100 lg:inline-block
block" %>
Now we have a quick way to hit the pricing page.
Adding pricing page front-end code
I added some mocked-up code to mimic what a subscription to a content-based offering might include. That lives inside app/views/static/pricing.html.erb
. It's very basic so don't judge me too hard.
<!-- app/views/static/pricing.html.erb-->
<div class="max-w-xl mx-auto p-8 rounded-xl shadow-xl mt-6">
<div class="h-[200px] bg-gray-100 overflow-hidden mb-6 rounded-lg">
<img
src="https://source.unsplash.com/random/800x400"
class="bg-gray-100 object-cover rounded-lg"
/>
</div>
<h1 class="font-black text-3xl mb-2">Subscribe for access</h1>
<p class="text-lg text-gray-700 mb-3">
You'll get never before seen content, an extra article per week, and access
to our private community.
</p>
<div class="grid grid-cols-2 gap-6">
<button
class="px-3 py-4 bg-teal-600 block w-full text-center rounded font-bold text-white shadow-sm"
>
Subscribe for $9/mo
</button>
<button
class="px-3 py-4 bg-indigo-600 block w-full text-center rounded font-bold text-white shadow-sm"
>
Subscribe for $90/yr
</button>
</div>
</div>
The main component that will hook into is the button
element. In a future step, we'll make these more oriented for Rails specifically.
Back to Stripe
Depending on your offering you can create products and prices in Stripe's dashboard.
Here I'll create a month and an annual option. To be generic I called the product Premium Access
. Our monthly price will be $9
and our annual price will be $90
(two months free to entice more yearly subscriptions π₯).
To summarize, we have a single product with two prices.
- Premium Access - Monthly @ $9/mo
- Premium Access - Annual @ $90/yr
You'll need the price IDs generated by Stripe to do the bulk of the work ahead.
Modeling Subscriptions
To keep track of our subscriptions in-app we can create a subscriptions table in the database and associate Stripe data with that subscription and the user subscribing. Note: This section assumes your app already contains a User model. We have one thanks to the bundled Devise gem within my template.
Let's generate a Subscription
model.
It will have the following columns:
plan
-string
(The plan will allow us to assign which product the user is actively subscribed to.)customer_id
-string
(Allows us to associate a customer to a subscription for easier retrieval)subscription_id
-string
(The id given to us by stripe for each newly generated subscription.)status
-string
(Determines the given state a subscription is in. Can be [incomplete
,incomplete_expired
,trialing
,active
,past_due
,canceled
,unpaid
]).interval
-string
(The billing interval of the given plan)current_period_end
-datetime
(When the subscription ends next)current_period_start
-datetime
(When the subscription renews next)- 'user
-
integer` - a way to tie a user to a subscription in our app
rails generate model Subscription plan_id:string customer_id:string user:references status:string current_period_end:datetime current_period_start:datetime interval:string subscription_id:string
invoke active_record
create db/migrate/20210627172041_create_subscriptions.rb
create app/models/subscription.rb
invoke test_unit
create test/models/subscription_test.rb
create test/fixtures/subscriptions.yml
Inside the migration file we have the following:
class CreateSubscriptions < ActiveRecord::Migration[7.0]
def change
create_table :subscriptions do |t|
t.string :plan
t.string :customer_id
t.string :subscription_id
t.references :user, null: false, foreign_key: true
t.string :status
t.string :interval
t.datetime :current_period_end
t.datetime :current_period_start
t.timestamps
end
end
end
Let's migrate the change into the database:
rails db:migrate
Generating the new model also created a subscription.rb
file in app/models
and inside it has the following:
# app/models/subscription.rb
class Subscription < ApplicationRecord
belongs_to :user
end
This effectively associates a user to a Subscription. We need to assign the User model manually now. A user could have either one subscription at a time or many depending on how you want your app to function. I think allowing multiple subscriptions is fine personally but you be the judge. With that in mind, we'll use a has_many
association on the user model.
# app/models/user.rb
class User < ApplicationRecord
has_person_name
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :subscriptions, dependent: :destroy # add this line <<--
end
You can ignore the has_person_name
and devise
lines for this part. The key thing to add is the has_many :subscriptions
bit. You'll notice I also appended the dependent: :destroy
suffix to that line of code. This means that if ever a user deletes their account, the associated subscription will also be deleted.
To be more thorough here you probably want to add a callback function BEFORE a user is deleted to also ping the Stripe API. This is to ensure their subscription is canceled on Stripe's end and they don't get charged again.
Add stripe_id
to users
We need to associate the stripe_id
to a given User should they subscribe. Our current users' table doesn't have this column. Let's add it now:
rails generate migration add_stripe_id_to_users stripe_id:string
Even though IDs are often integers, a third-party API could update at some point. Doing so means keeping our options open. If we were to use an integer type that binds us to numbers only so opting for a string helps make this more flexible.
rails db:migrate
Create a checkout session
Back in the old days before SCA (strong customer authentication) was a requirement for some countries, you could generate a token on the front end, pass it to the back end, and proceed to charge a given customer's credit card.
Due to security and identity concerns of late, there's a newer protocol now being enforced with newer versions of Stripe's API.
The new protocol requires backend session generation to occur first. You can then pass data back to the front-end and complete checkout. The front end captures the card data, matches a secret key id, and then is shipped back to Stripe via serverside call. It sounds complicated but it's not the hardest thing to code. Let's dig in.
Let's first create a route for our Stripe Checkout instance.
# added to config/routes.rb
namespace :purchase do
resources :checkouts
end
resources :subscriptions
Here I declared a namespace for organizational purposes (feel free to customize). The URL would look like /purchase/checkouts/new
when we create a new Stripe Checkout session (coming up). With this routing added we need the associated controller to make this all work.
The end goal is to be able to submit the plan a customer chooses to an endpoint that will eventually subscribe that customer using Stripe's Checkout UI and API.
The Checkouts controller
We need some controller logic to handle each request now. To do this let's create one manually called checkouts_controller.rb
. Because of the namespace, we introduced this will live within a module (or folder) in app/controllers
called purchase
Our controller without any logic looks like this:
# app/controllers/purchase/checkouts_controller.rb
class Purchase::CheckoutsController < ApplicationController
before_action :authenticate_user!
def create
end
end
Setting up application credentials
Since Rails 5.2 there has been a new handy way to create encrypted secret keys inside apps instead of the legacy secrets.yml
file or environment variables localized to your machine/servers.
This essentially allows you to version your API keys and access codes securely assuming you have a master.key
file.
On the first run, the command below will generate and decrypt a file called credentials.ymc.enc
in the config
folder. That command looks like this:
rails credentials:edit
Your mileage may vary here and it might spit back an error saying to choose which code editor to open the credentials file with. I tend to use VS code so passing EDITOR=code --wait rails crendentials:edit
should do the trick.
When that file opens you'll see a YAML
file appear which is just a space to add any API keys that are sensitive and shouldn't be shared. Stripe's publishable key should be the only one you expose to the front-end of your apps.
I'll be using test keys of course for this tutorial but you may eventually need live keys.
You can either swap those out inside your main credentials file or generate environment-specific application credentials. That might look like rails credentials:edit --environment=development
for example where the live keys would live in the main credentials file (rails credentials:edit
) and any test keys live in the development version. There's a whole can of worms with this feature of Rails. I recommend checking the documentation for more information. Perhaps I'll do a future guide just on credentials.
At this point, we don't have our pricing IDs added to our secret credentials like we previously did with the Stripe API keys. We can add those now along with our main API keys:
# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: XXXXXXXX
stripe:
secret_key: sk_test_XXXXXXXXXXXXXX
public_key: pk_test_XXXXXXXXXXXXXX
pricing:
monthly: price_XXXXXXXXXXXX
annual: price_XXXXXXXXXXXXX
Here's how mine currently looks. I removed my keys for obvious reasons. Save the file and close it and you should see that it has been encrypted successfully.
If you want to give your keys a trial run you can verify they output via rails console:
rails console
> Rails.application.credentials.dig(:stripe, :pricing, :monthly)
#=> price_XXXXXXXXXXXX
To kick off a new Stripe Checkout session we can call the associated class via the Stripe gem we installed previously. The goal here is to pass the plan type from the front end to the controller, assign the appropriate price_id
(the ones we created in Stripe previously) and launch a new Stripe checkout instance. A new overlay should appear with the product/pricing data and a credit card form. To get the Stripe Checkout overlay to appear we need to return a session_id to a Stripe hosted page directly.
More view code
To get Stripe data from the front-end to the backend we need to ensure we can first have access to our Stripe publishable API key. Let's add a meta
tag to the main layout file to have it available globally. In the template I am using I leverage a partial called _head.html.erb
that gets injected inside the application.html.erb
file. Here's what I have at the moment.
<!-- app/views/shared/_head.html.erb-->
<head>
<title>
<%= content_for?(:title) ? yield(:title) : "Kickoff Tailwind" %>
</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<%= stylesheet_link_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag "application", defer: true %>
<%= javascript_importmap_tags %>
<%= javascript_include_tag 'https://js.stripe.com/v3/', 'data-turbolinks-track': 'reload' %>
</head>
Passing the Stripe API key
While we could supply our Stripe private key in every instance we need it, it's much easier to make it globally accessible in the application controller. You could previously create a new initializer file in config/initializers
but I kept running into issues with that. For the time being, I just added it to the ApplicationController
file for quick reusability across the app.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :set_stripe_key # add this line
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
# add the code below
private
def set_stripe_key
Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
end
The code you see here is part of our Devise installation included in my template. I added a new set_stripe_key
private method that gets called before every action. This is a code smell to a degree since we'll be setting the API key in every request but for now, it should do.
Pricing page updates
Let's update the view quickly to check if the end-user is signed in or not and link the payment buttons accordingly.
<!-- app/views/static/pricing.html.erb -->
<!-- more code here--->
<% if user_signed_in? %>
<%= form_tag purchase_checkouts_path, method: :post, data: { turbo: false } do %>
<input type="hidden" name="price_id" value="<%= Rails.application.credentials.dig(:stripe, :pricing, :monthly) %>" />
<%= submit_tag "Subscribe for $9/mo", data: { disable_with: "Hold please..." }, class: "px-3 py-4 bg-teal-600 block no-underline w-full text-center rounded font-bold text-white shadow-sm" %>
<% end %>
<%= form_tag purchase_checkouts_path, method: :post, data: { turbo: false } do %>
<input type="hidden" name="price_id" value="<%= Rails.application.credentials.dig(:stripe, :pricing, :annual) %>" />
<%= submit_tag "Subscribe for $90/yr", data: { disable_with: "Hold please..." }, class: "px-3 py-4 bg-indigo-600 block no-underline w-full text-center rounded font-bold text-white shadow-sm" %>
<% end %>
<% else %>
<%= link_to "Subscribe for $9/mo", new_user_session_path, class: "px-3 py-4 bg-teal-600 block no-underline w-full text-center rounded font-bold text-white shadow-sm" %>
<%= link_to "Subscribe for $90/yr", new_user_session_path, class: "px-3 py-4 bg-indigo-600 no-underline block w-full text-center rounded font-bold text-white shadow-sm" %>
<% end %>
<!-- more code here--->
This is kind of a mess but gets the job done.
What's going on here?
If a user isn't signed in we can link to the sign-in page. We do this because we eventually want to associate a user with a subscription once we charge them. Doing so requires getting a user's data so them being authenticated within the app is crucial.
If they are signed in we can render the checkout buttons that act as the entry point to Stripe's Checkout solution.
Each button acts as a form that POSTs to the app passing an associated price_id
we defined in our credential files in a previous step. You can pass additional parameters here but let's keep it simple for now.
I passed a price_id
parameter as a hidden field the user doesn't see on the front end. This ultimately allows us to pass the right price relative to the plan to Stripe's API.
The low code way
Since Stripe has a built-in checkout I'm going to leverage it for the sake of not reinventing the wheel. As your app gets more complicated and you need more control, you can create an entirely custom checkout experience should you want to.
Inside the checkouts_controller
I added the new Stripe Checkout Session instance. You'll see some local variables that hook into what pricing a user has chosen on the front end. We'll use a parameter passed in the request to denote what pricing to use.
# app/controllers/purchase/checkouts_controller.rb
class Purchase::CheckoutsController < ApplicationController
before_action :authenticate_user!
def create
price = params[:price_id] # passed in via the hidden field in pricing.html.erb
session = Stripe::Checkout::Session.create(
customer: current_user.stripe_id,
client_reference_id: current_user.id,
success_url: root_url + "success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: pricing_url,
payment_method_types: ['card'],
mode: 'subscription',
customer_email: current_user.email,
line_items: [{
quantity: 1,
price: price,
}]
)
#render json: { session_id: session.id } # if you want a json response
redirect_to session.url, allow_other_host: true
end
end
The Stripe docs do better justice than I would be explaining everything happening here but we are doing the following:
- Passing which plan a customer wants in the params/request. From that, we assign a local
price_id
variable based on theprice_id
s we got back when we created the two prices for our product in the Stripe account. - Creating a local
session
variable and assigning it to the Checkout::Session object. - The
session
object allows us to assign a customer's stripe id (if already present). If nothing is there then a new one will be created. - The
client_reference_id
is a way to store data about customers from your application. Here we just use the user id which comes in handy later. - Call back URLs.
success
(post-purchase) andcancel
(where customers go if they back out). Notice the{CHECKOUT_SESSION_ID}
parameter. This is resupplied by Stripe automatically once the charge is successful. Sadly, I needed to write the URL in a rather hacky way to get it to work. You can't use full rails URL helpers here. - A way to establish payment method types.
card
is the most common but you could also accept Apple Pay, Google Pay, etc... - The
mode
. This could be payment, subscription, setup. (read more here about this) - line items - Here you can add a bunch of prices/products at once or do like we are and pass one during the session creation. This depends on your preferences and UI. I like to have separate actions for different plans personally.
- Much more data can be added to this session variable like taxes, shipping details, and additional payment types.
- The method called
allow_other_host: true
is a newer security feature in Rails 7. You can bypass a host and redirect to the outside of the main application if set to true.
Add a success page
After a successful checkout, we need somewhere for users to go. Add this to your routes to make this sync up on your end:
# config/routes.rb
namespace :purchase do
resources :checkouts
end
get "success", to: "checkouts#success"
This change allows us to assign a success URL that the user is directed to following a successful purchase.
This addition points to the app/controllers/purchase/checkouts_controller.rb
file and a new #success
action in it. Finally, be sure to create a new view inside app/views/purchase/checkouts
called success.html.erb
. We'll add code to it in a bit.
Testing things out
This is a lot to cover! Congrats on making it this far if you have.
Hopefully, you see something similar to this when clicking a subscribe button at this point. Each button should fire up a Stripe Checkout modal that displays different plan information.
Upon checkout out using test card numbers and data, you should navigate successfully to a success page.
Bonus points
Notice the email field on the Stripe Checkout UI? Wouldn't be cool if we can prefill those with the logged-in user's email address? Well, we totally can π¦Ύ.
Modify the session object like below:
# app/controllers/purchase/checkouts_controller.rb
class Purchase::CheckoutsController < ApplicationController
before_action :authenticate_user!
def create
session = Stripe::Checkout::Session.create(
customer: current_user.stripe_id,
customer_email: current_user.email, # I added this line
client_reference_id: current_user.id,
success_url: root_url + "success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: pricing_url,
payment_method_types: ['card'],
mode: 'subscription',
line_items: [{
quantity: 1,
price: price,
}]
)
end
def success
end
end
This updates the UI to match something like the following:
Success view
While it may look like we are done, there's a bit more work to do following the purchase flow. Upon a successful charge, we still need to send our new customers somewhere. We can do that by appending some simple logic to the success
action.
class Purchase::CheckoutsController < ApplicationController
#...
def success
session = Stripe::Checkout::Session.retrieve(params[:session_id])
@customer = Stripe::Customer.retrieve(session.customer)
end
#...
end
After a successful charge, we redirect the user to the success page where we retrieve their name via the Stripe API. The session_id
is what we use to query to API and obtain customer data.
Be sure to create a new view in app/views/purchase/checkouts
called success.html.erb
to properly render the view.
I made some really simple boilerplate code there you can tweak as you wish:
<!-- app/views/purchase/checkouts/success.html.erb-->
<div class="max-w-xl mx-auto p-8 rounded-xl shadow-xl mt-6">
<div class="h-[200px] bg-gray-100 overflow-hidden mb-6 rounded-lg">
<img
src="https://source.unsplash.com/random/800x400"
class="bg-gray-100 object-cover rounded-lg"
/>
</div>
<div>
<h1 class="font-black text-3xl mb-3 text-center">Thanks for subscribing, <%= @customer.name %>!</h1>
<%= link_to "Continue", root_url, class: "px-3 py-4 bg-black no-underline block w-full text-center rounded font-bold text-white shadow-sm" %>
</div>
</div>
Try purchasing a plan in test mode and see if you make it through π€π€.
It worked for me! Here's a screenshot of the test transaction
Listening to webhooks and subscribing a user
While getting Stripe's checkout to work and make a successful transaction is a huge win, we don't yet tie that state of that subscription to our actual application. If you recall earlier we made a Subscription
model. This will be that glue that connects a user to a subscription and Stripe's API thereafter.
Today, the most recommended way to ensure a successful transaction occurred via Stripe is to listen to events via webhooks. If you're new to webhooks don't worry. At the surface, it's just a way for a service to cast events as they happen. Using those events your application can receive data about them through a pre-defined endpoint of your choice. Based on what occurs you can perform nearly any kind of operation in an automated way.
Testing webhooks with Stripe CLI
Testing webhook events locally is a lot easier with Stripe's new CLI tool. If you haven't already, be sure to install it.
brew install stripe/stripe-cli/stripe
With that installed you can set up your local environment to simulate webhook events.
Start by logging in via Stripe CLI
stripe login
This should redirect you to log in if you haven't already and then verify a unique string that output in your console. Try visiting this page for a step-by-step guide if you get stuck.
When you log in successfully you are given a webhook secret key that lasts a maximum of 90 days. I tend to throw this in my application crendentials file and access it that way for security's sake. It's easy enough to revoke the secret key from your Stripe account so I'll leave it up to you how "secure" you want to be.
bin/rails credentials:edit
Next to our other Stripe API keys, I added one more for the webhook_signing_secret
key:
stripe:
secret_key: sk_test_XXXXXXXXXXXXXXXXXXXXX
public_key: pk_test_XXXXXXXXXXXXXXXXXXXXX
webhook_signing_secret: whsec_XXXxXXXXXXXXXXXXXXXXX
pricing:
monthly: price_XXXXXXXXXXXXXXXXXXXXX
annual: price_XXXXXXXXXXXXXXXXXXXXX
Generate a webhook events controller
We need a new endpoint to accept events from Stripe's webhooks. Let's create a unique controller to handle this logic.
rails g controller webhooks --skip-helper --skip-assets
create app/controllers/webhooks_controller.rb
invoke erb
create app/views/webhooks
invoke test_unit
create test/controllers/webhooks_controller_test.rb
A couple of notes about the above command:
--skip-helper
skips any helpers that get added automatically toapp/helpers
when you run a default controller generator command.--skip-assets
skips adding anyCSS
files.
Configure routes
With the new controller in place, we need a custom route to set up the endpoint for our API.
# config/routes.rb
Rails.application.routes.draw do
#...
resources :webhooks, only: :create
#...
end
In the world of rails, the create
action is usually considered a POST
request type in the RESTful model.
Controller logic
Inside the WebhooksController
I added a new create
action. Inside it, I'll add some code to handle the input requests.
An important step to not ignore is to bypass the CSRF functionality that comes baked into the framework. In this case, we don't want to enforce cross-site request forgery. We'll check the authenticity of the request in a different way.
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
# Do stuff
end
end
Test it out
Let's fire up the local forwarding of simulated Stripe events to our new route for webhooks
stripe listen --forward-to localhost:3000/webhooks
We can test things out by firing a test event via the console. In a new tab type the following and hit return.
stripe trigger checkout.session.completed
If you look back in the other tab running the stripe listen
session you should see a paper trail of activity which hopefully verifies it's working even though we aren't parsing any data just yet.
Handling events
With everything in place we need to think about how we're going to process the data that comes in. To help plan I created a quick outline of concerns to attend to.
- What kind of event is it?
- What should we do with this specific event?
- Verify signatures and parse events
- Kick-off background jobs/other logic to update subscription/user data in-app.
To parse the events we need to handle them as JSON
which is pretty typical these days. In the controller, we need some additional logic to parse JSON
with ruby and then catch specific events we're interested in responding to.
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
# docs: https://stripe.com/docs/payments/checkout/fulfill-orders
# receive POST from Stripe
payload = request.body.read
signature_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :webhook_signing_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, signature_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
render json: {message: e}, status: 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
render json: {message: e}, status: 400
return
end
# Handle the event
case event.type
when 'checkout.session.completed'
# Payment is successful and the subscription is created.
# Provision the subscription and save the customer ID to your database.
when 'checkout.session.async_payment_succeeded'
# Some payments take longer to succeed (usually noncredit card payments)
# You could do logic here to account for that.
when 'invoice.payment_succeeded'
# Continue to provide the subscription as payments continue to be made.
# Store the status in your database and check when a user accesses your service.
# This approach helps you avoid hitting rate limits.
when 'invoice.payment_failed'
# The payment failed or the customer does not have a valid payment method.
# The subscription becomes past_due. Notify the customer and send them to the
# customer portal to update their payment information.
else
puts "Unhandled event type: #{event.type}"
end
end
end
There's a lot here but the gist is the following:
- When we receive a
POST
request to our app from Stripe we can absorb the data and parse it. - When parsing it we can check its authenticity and verify it with the signing secret key we pass to the
Stripe::Webhooks.construct_event
call. - Finally, we use the event and compare what type of event it is we are receiving to do more operations automatically in-app.
Keeping views from breaking
If you managed to subscribe a customer congrats! At this point, our view logic isn't compensating for subscribers so we can adjust our pricing page to compensate. This adds additional logic to the view but we can extract some of it.
There are three states we need to account for:
- Logged out users
- Logged in unsubscribed users
- Logged in subscribed users.
We already account for number 1 above but now we'll need to figure out 2 and 3.
First, let's add a method to the user model to check for any active subscriptions. Since our app allows a user to have many subscriptions we need to treat subscriptions as a multiple when finding data on them.
### app/models/user.rb
class User < ApplicationRecord
# more code is here but ommitted for brevity
def subscribed?
subscriptions.where(status: 'active').any?
end
end
Since we previously defined subscriptions in the app we can query for any that might be active for a given user. If so, it's safer to assume that the user has an active subscription and is indeed subscribed.
This can trickle down to the view layer like so:
<!-- app/views/static/pricing.html.erb -->
<div class="max-w-xl mx-auto p-8 rounded-xl shadow-xl mt-6">
<div class="h-[200px] bg-gray-100 overflow-hidden mb-6 rounded-lg">
<img
src="https://source.unsplash.com/random/800x400"
class="bg-gray-100 object-cover rounded-lg"
/>
</div>
<h1 class="font-black text-3xl mb-2">Subscribe for access</h1>
<p class="text-lg text-gray-700 mb-3">
You'll get never before seen content, an extra article per week, and access
to our private community.
</p>
<div class="grid grid-cols-2 gap-6">
<% if user_signed_in? && !current_user.subscribed? %> <%= form_tag
purchase_checkouts_path, method: :post, data: { turbo: false } do %>
<input
type="hidden"
name="price_id"
value="<%= Rails.application.credentials.dig(:stripe, :pricing, :monthly) %>"
/>
<%= submit_tag "Subscribe for $9/mo", data: { disable_with: "Hold please..."
}, class: "px-3 py-4 bg-teal-600 block no-underline w-full text-center
rounded font-bold text-white shadow-sm" %> <% end %> <%= form_tag
purchase_checkouts_path, method: :post, data: { turbo: false } do %>
<input
type="hidden"
name="price_id"
value="<%= Rails.application.credentials.dig(:stripe, :pricing, :annual) %>"
/>
<%= submit_tag "Subscribe for $90/yr", data: { disable_with: "Hold
please..." }, class: "px-3 py-4 bg-indigo-600 block no-underline w-full
text-center rounded font-bold text-white shadow-sm" %> <% end %> <% elsif
user_signed_in? && current_user.subscribed? %>
<div
class="bg-yellow-100 p-6 rounded-lg text-center text-yellow-900 col-span-2"
>
<p>
You're already subscribed. <%= button_to "Manage your subscription",
billing_path, method: :post, data: { turbo: false }, class: "underline"
%>.
</p>
</div>
<% else %> <%= link_to "Subscribe for $9/mo", new_user_session_path, class:
"px-3 py-4 bg-teal-600 block no-underline w-full text-center rounded
font-bold text-white shadow-sm" %> <%= link_to "Subscribe for $90/yr",
new_user_session_path, class: "px-3 py-4 bg-indigo-600 no-underline block
w-full text-center rounded font-bold text-white shadow-sm" %> <% end %>
</div>
</div>
Notice now we have an if-else if and else scenario in the view. It feels gross but does the job. The new method on the user model can be chained so current_user.subscribed?
effectively extracts so more advanced logic behind the scenes.
Handling necessary events
While there is a multitude of events you could respond to with webhooks I want to keep this pretty simple based on the code in our controller thus far. There's also a ton of extraction you could do here but for brevity's sake, I'll keep it kind of scrappy.
The main event we'll look for to confirm payment is the checkout.session.completed
event. This essentially means a customer has gone completely through the payment on Stripe's checkout UI. There is an additional even that can listen for async payments (ones that happen later) but I'll save that for another time perhaps.
checkout.session.completed
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
def create
#.... tons of other code
# Handle the event
case event.type
when 'checkout.session.completed'
# If a user doesn't exist we definitely don't want to subscribe them
return if !User.exists?(event.data.object.client_reference_id)
# Payment is successful and the subscription is created.
# Provision the subscription and save the customer ID to your database.
fullfill_order(event.data.object)
else
# more events + logic
end
end
private
def fullfill_order(checkout_session)
# Find user and assign customer id from Stripe
user = User.find(checkout_session.client_reference_id)
user.update(stripe_id: checkout_session.customer)
# Retrieve new subscription via Stripe API using susbscription id
stripe_subscription = Stripe::Subscription.retrieve(checkout_session.subscription)
# Create new subscription with Stripe subscription details and user data
Subscription.create(
customer_id: stripe_subscription.customer,
current_period_start: Time.at(stripe_subscription.current_period_start).to_datetime,
current_period_end: Time.at(stripe_subscription.current_period_end).to_datetime,
plan: stripe_subscription.plan.id,
interval: stripe_subscription.plan.interval,
status: stripe_subscription.status,
subscription_id: stripe_subscription.id,
user: user,
)
end
end
To fulfill an order I made a new private method called fullfill_order
(this should probably be extracted to a background job or plain old ruby object elsewhere). We pass the checkout session object as the main argument. From there I do some preemptive logic to first make sure a user exists in the database. If they don't we simply return and don't subscribe them. If they do exist we'll find them by id and also update the stripe_id
column for the user to include Stripe's customer id attribute. This is handy for calling the API later for viewing any subscription, invoice, or charge data related to a given user.
Once we have a user to work with I retrieve the newly added subscription from the Stripe API using the checkout session subscription value. That is stored in a local variable I called stripe_subscription
which we then use to create a local subscription object in our database that maps to the same user. We add a few handy attributes including plan_id, customer_id, interval, start and end dates, and status.
Assuming the webhook event fires successfully and our app responds to the POST request at /webhooks
from Stripe, customers should be able to successfully subscribe now!
Why store our subscription data?
This is a good practice to limit rate requests on the Stripe API. While not ideal to host this data allows us to assume a given user is subscribed or not based on events firing back from Stripe itself. It also is a great way to document the state of a subscription so we can properly notify customers as needed.
I'll be using the Stripe customer portal to have users manage their subscriptions via Stripe instead of in our app. This makes things way easier and more efficient.
invoice.payment_failed
If an invoice payment fails we can direct the user to the customer portal to manage their billing details. The failure is usually a result of an expired card or something similar.
invoice.payment_succeeded
If an invoice.payment_succeeded
event is fired we can query for the subscription and update the new periods, status, and interval. This ultimately maintains the life of the subscription in-app and syncs with Stripe. It also gives a chance to notify customers of a successful payment.
Stripe offers a complete invoicing solution that automates this for you. I'll make use of it in this guide but you could just as easily roll your own invoicing if you want more control. Keep in mind doing so requires turning off invoicing in Stripe as well as adding the logic for receipts(emails, data, additional webhook events, etc...). And just like subscription logic you can listen to invoice events and perform actions after being triggered via webhooks.
To update the code to handle this logic I modified the webhooks controller once more.
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
#... lots of code removed for brevity
# Handle the event
case event.type
when 'checkout.session.completed'
# do stuff
when 'invoice.payment_succeeded'
# return if a subscription id isn't present on the invoice
return unless event.data.object.subscription.present?
# Continue to provision the subscription as payments continue to be made.
# Store the status in your database and check when a user accesses your service.
stripe_subscription = Stripe::Subscription.retrieve(event.data.object.subscription)
subscription = Subscription.find_by(subscription_id: stripe_subscription)
subscription.update(
current_period_start: Time.at(stripe_subscription.current_period_start).to_datetime,
current_period_end: Time.at(stripe_subscription.current_period_end).to_datetime,
plan: stripe_subscription.plan.id,
interval: stripe_subscription.plan.interval,
status: stripe_subscription.status,
)
# Stripe can send an email here for invoice paid attempts. Configure in your account OR roll your own below
when 'invoice.payment_failed'
# do stuff
else
puts "Unhandled event type: #{event.type}"
end
end
end
end
invoice.payment_failed
Finally, handling an invoice.payment_failed
event allows you to cast an update (usually an email) to the customer to take action to update their billing information. This section is where I'll get into the new Stripe Customer Portal solution which is a one-stop place for customers to manage their billing details directly with Stripe. This saves a lot of logic needing to be created from scratch on our end but with that decision, you sacrifice some control. All in all, unless you're a highly established business or just want that much control Stripe Customer Portal should be plenty to get billing done in a pinch.
I'll revisit this section in a bit.
Stripe customer portal
Before we can write the event logic for invoice.payment_failed
I want to get the customer portal set up. This will be at an endpoint we can redirect customers to when necessary.
Defining routes
We need a new endpoint on our app to send customers to. This will be a place where we create another session and fire up the customer portal.
# config/routes.rb
resources :billings, only: :create
Create a controller
With the new route added we'll need to add a billings controller. Inside the file, we can set up the portal instance. Rails prefers to make controller namespaces plural if you're wondering.
# app/controllers/billings_controller.rb
class BillingsController < ApplicationController
before_action :authenticate_user!
def create
session = Stripe::BillingPortal::Session.create({
customer: current_user.stripe_id,
return_url: root_url,
})
redirect_to session.url, allow_other_host: true #hits stripe directly
end
end
This small bit of code accounts launching the Stripe customer portal. We grab the previously saved customer id from Stripe and pass it to customer
. Then set the return URL to our root URL for now. Be sure to use URL
helpers here and not path
helpers.
Let's test this out on our pricing page. If you haven't already subscribed you'll need to do that to see the UI that links to manage the currently subscribed user's subscription. I'll update the link to manage that subscription to the new billing path.
<% if user_signed_in? && !current_user.subscribed? %> <%# checkout buttons go
here %> <% elsif user_signed_in? && current_user.subscribed? %>
<div
class="bg-yellow-100 p-6 rounded-lg text-center text-yellow-900 col-span-2"
>
<p>
You're already subscribed. <%= button_to "Manage your subscription",
billings_path, data: { turbo: false }, class: "underline" %>.
</p>
</div>
<% else %> <%# login buttons go here %> <% end %>
The mainline to focus on is this bit of erb/html
<p>
You're already subscribed. <%= button_to "Manage your subscription",
billings_path, { turbo: false }, class: "underline" %>.
</p>
`
I swapped what was a link_to
helper for a button_to
helper which is essentially a form underneath the surface. Based on our routing we'll be creating a POST
request when this gets clicked.
If upon clicking you receive an error that reads:
You canβt create a portal session in test mode until you save your customer portal settings in test mode at https://dashboard.stripe.com/test/settings/billing/portal.
Visit that link to enable settings to use in test mode. Be sure to configure whatever settings are required and also the optional ones. Here's a screenshot of the demo app I'm using.
With those settings added, you can try to create a new customer portal billing session again.
Below I show it working!
Revisiting invoice.payment_failed
With the portal working, we now have a place to send active subscribers to manage their plans. I made it possible in Stripe to also update their plan choice. Doing this means we'll need to account for some additional webhook events.
For the invoice.payment_failed
event type I want to send an email to the user about the occurrence. In that email, we can add a link to their customer portal.
Add a mailer
rails g mailer subscription payment_failed
create app/mailers/subscription_mailer.rb
invoke erb
create app/views/subscription_mailer
create app/views/subscription_mailer/payment_failed.text.erb
create app/views/subscription_mailer/payment_failed.html.erb
invoke test_unit
create test/mailers/subscription_mailer_test.rb
create test/mailers/previews/subscription_mailer_preview.rb
The command above creates a SubscriptionMailer with payment_failed
methods baked in. We'll need to amend things a bit but this gives us a good starting point.
Within the invoice.payment_failed
event we can listen for a few values we'll need to send the email.
We can test the event using Stripe's CLI tool by running:
stripe trigger invoice.payment_failed
I tend to throw a debugger
statement in the case statement to help me figure out what to query from Stripe's API.
We'll want to find the customer ID using event.data.object.customer
. In my case, it returns a string that starts with cus_xxxxx..
. We can use that customer id to look up the user we'll need to send the email to in our app.
class WebhooksController < ApplicationController
def create
# code omitted for brevity
case event.type
# more code....
when 'invoice.payment_failed'
# The payment failed or the customer does not have a valid payment method.
# The subscription becomes past_due so we need to notify the customer and send them to the customer portal to update their payment information.
# Find the user by stripe id or customer id from Stripe event response
user = User.find_by(stripe_id: event.data.object.customer)
# Send an email to that customer detailing the problem with instructions on how to solve it.
if user.exists?
SubscriptionMailer.with(user: user).payment_failed.deliver_now
end
else
puts "Unhandled event type: #{event.type}"
end
end
end
You can trigger this logic by running stripe trigger invoice.payment_failed
once more.
In my rails server logs I'm able to see the email was indeed sent:
SubscriptionMailer#payment_failed: processed outbound mail in 9.8ms
Delivered mail 61f42cd417751_1009d4628-3d9@XXXXXXXXXXXX (9.4ms)
Date: Fri, 28 Jan 2022 11:50:12 -0600
From: [email protected]
To: [email protected]
Message-ID: <61f42cd417751_1009d4628-3d9@XXXXXXXXXXX>
Subject: Payment failed
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_61f42cd416df3_1009d4628-4a4";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_61f42cd416df3_1009d4628-4a4
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Subscription#payment_failed
Hi, find me in app/views/subscription_mailer/payment_failed.text.erb
----==_mimepart_61f42cd416df3_1009d4628-4a4
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<h1>Subscription#payment_failed</h1>
<p>
Hi, find me in app/views/subscription_mailer/payment_failed.html.erb
</p>
</body>
</html>
----==_mimepart_61f42cd416df3_1009d4628-4a4--
The email template needs some TLC still but we can confirm it works π.
Update the mailer
Knowing our email sends we can set up the logic to send to the correct customer.
# app/mailers/subscription_mailer.rb
class SubscriptionMailer < ApplicationMailer
def payment_failed
@user = params[:user]
mail to: @user.email, subject: "Payment attempt failed"
end
end
I modified the method to use the following code. We are passing an instance of the user to the mailer class for use within the view. We'll need this to address the end-user more personally. I updated the subject line of the email and also sent it directly to the user's email address.
Previewing the template
Rails comes with a handy preview tool found in test/mailers/previews
. Look for the file subscription_mailer_preview.rb
. Inside it we'll need to change it to the following:
# test/mailers/previews/subscription_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/subscription_mailer
class SubscriptionMailerPreview < ActionMailer::Preview
def payment_failed
SubscriptionMailer.with(user: User.first).payment_failed
end
end
This is for the sake of viewing the email in the browser at http://localhost:3000/rails/mailers
. There you will find all the mailer templates that have been generated if created using a rails generator.
Right now the template looks awful so let's at least add some updated copy and a link to the user's account settings where they can update their billing information.
Update the email template
When you generate a mailer with a method on the command line there are two templates created. One HTML and one text. I'm going to delete the text-only one for now as it's not necessary.
<!-- app/views/subscription_mailer -->
<h1>Payment attempt failed</h1>
<p>Hi <%= @user.first_name %>,</p>
<p>It looks like your latest automated payment attempt failed</p>
<p>
To not see a lapse in your subscription, we suggest updating your billing
information as soon as possible
</p>
<p>
<%= link_to "Update billing information", edit_user_registration_url, class:
"underline", target: :_blank %>
</p>
Notice I linked to edit_user_registration_url
here instead of directly to the billing portal. We can't reliably embed a link/button/form in an email to submit a post request. This is kind of by design for security purposes. Instead, we'll redirect the user to their actual account where we can add a button to do the billing portion. Let's add that now.
Add billing button to account settings
The template I'm using comes preconfigured with Devise which handles all our user authorization. We can hook into the preexisting views to add a call to action to manage billing information. I updated the template quickly to reflect. The code below needs to be outside of the layout rendering in the view. This is because the button elements would cause the form to submit if they were within. We don't want that.
<!-- app/views/devise/registrations/edit.html.erb-->
<!-- code above omitted for brevity -->
<%= render layout: "auth_layout", locals: { title: "Edit account" } do %> <%=
form_for(resource, as: resource_name, url: registration_path(resource_name),
html: { method: :put }, data: { turbo: false }) do |f| %>
<!-- more code here -->
<div class="mb-6">
<%= f.submit "Update", class: button_class(theme: "primary", variant:
"expanded") %>
</div>
<% end %>
<hr class="mb-6" />
<div class="flex justify-between items-center mb-6">
<div>
<%= button_to("Manage billing", billings_path, data: {turbo: false}, class:
button_class(theme: "dark")) %>
</div>
<div>
<%= button_to("Cancel my account", registration_path(resource_name), data: {
confirm: "Are you sure?", turbo: false }, method: :delete, class:
"underline") %>
</div>
</div>
<% end %>
The key action I added was the "Manage Billing" button. We'll want to additionally make sure a user is subscribed before displaying this so you can extend things one step further.
<!-- app/views/devise/registrations/edit.html.erb -->
<div class="flex justify-between items-center mb-6">
<% if current_user.subscribed? %>
<div>
<%= button_to("Manage billing", billings_path, data: {turbo: false}, class:
button_class(theme: "dark")) %>
</div>
<% end %>
<div>
<%= button_to("Cancel my account", registration_path(resource_name), data: {
confirm: "Are you sure?", turbo: false }, method: :delete, class:
"underline") %>
</div>
</div>
Clicking the Manage Billing
button now allows a user to do just that with Stripe Customer Portal.
Listening to additional events
We'll need to account for a few more events to make this come full circle. Those include:
Your implementation might require different events but below are the ones I'd suggest.
Subscription cancellation -
customer.subscription.updated
andcancel_at_period_end (needs to be set to false to cancel)
. Read this for more context.Subscription updates -
customer.subscription.updated
and check for subscription price differences - change plan
Subscription updates are mostly tailored to unique plan types that unlock a new tier in your product or service. You need to find out what price
a customer is currently subscribed to and update anything locally with the new price. This might look like going from a basic
plan to a pro
plan and vice versa.
For the sake of this guide, having subscribed monthly or yearly doesn't unlock anything different other than a discount over time if you opted for yearly. With this in mind, we don't need to listen for subscription changes for now as the monthly and yearly are the same offering.
Cancelling subscriptions
We don't want to have a user subscribed in-app but unsubscribed on Stripe so to accommodate this we need to listen for the customer.subscription.update
event. In the customer portal settings, I chose to allow a customer to cancel a plan but still have access through the current billing period. You can choose to end a subscription abruptly if you wish. That would be a different event called customer.subscription.deleted
.
If the customer doesn't resubscribe in the interim we can remove subscription access when the period expires.
This code is gnarly and should be extracted but I wanted to just be clear about what's happening.
case event.type
# more events here
when 'customer.subscription.updated'
# deletion
stripe_subscription = event.data.object
# if cancel_at_period_end is true the subscription will cancel
if stripe_subscription.cancel_at_period_end == true
subscription = Subscription.find_by(subscription_id: stripe_subscription.id)
if subscription.present?
# update the local subscription data with the status and other details that may have changed.
subscription.update(
current_period_start: Time.at(stripe_subscription.current_period_start).to_datetime,
current_period_end: Time.at(stripe_subscription.current_period_end).to_datetime,
interval: stripe_subscription.plan.interval,
plan: stripe_subscription.plan.id,
status: stripe_subscription.status
)
end
end
else
puts "Unhandled event type: #{event.type}"
end
If you recall we're checking a lot of places for a status
of active
on any subscriptions associated with a user. If a subscription lapses that status will change and thus unsubscribe the user on our end. You could also track this type of logic with some simple boolean properties on the user or subscription model. Given that our app supports multiple subscriptions (not that we offer them) I made it a bit more rugged.
Going live
While we've been testing these endpoints locally the real secret sauce is going live. Your /webhooks
endpoint needs to be an absolute URL that gets added to Stripe in your account. Look for the Webhooks
tab under "Developer". There's a section called "Hosted endpoints". Be sure to add your endpoint there when going live. That might look like https://demoapp.com/webhooks
for example.
Go on and build!
So we've come extremely far. Stripe Checkout and Stripe Customer Portal are huge leaps to much richer billing experiences for customers and the developers behind the scenes building it. With a few lines of code, you can have a full functioning checkout and customer admin tool that is entirely automated. What a time to be alive!
Categories
Collection
Part of the Ruby on Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.