How to Sell a One-time Purchase in Rails With Stripe
Tags: rails, ruby, stripe, tutorial
Published: 2022-06-15 17:46:39

*This article was originally written by [Jeffrey Morhous](https://www.honeybadger.io/blog/authors/jeffreymorhous/) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/sell-a-one-time-purchase-in-rails-with-stripe/).*

Used by millions of companies, Stripe provides payment infrastructure for applications to handle subscriptions and one-time purchases. Stripe Checkout allows us to easily accept card payments via a hosted payments page, which is crafted to increase conversions. Combining this with webhooks allows developers to sell products and subscriptions and then deliver them digitally.

While it's technically possible to handle payments yourself using an existing provider, Stripe provides a number of benefits. For starters, it's faster. Stripe has over 4,000 people working on making payments as simple, secure, and convenient as possible.  

Furthermore, having your payments 'powered by Stripe' is an easy way to build customer trust and increase conversion. Customers are typically hesitant to give their card or bank information to every website they visit, and rightfully so.

Stripe isn't the only provider in the online payment space. [Paddle](https://paddle.com/) is another huge player in the online payment space. Paddle offers a lot of features similar to Stripe, but for a slightly higher fee and an anecdotally worse developer experience. While several other competitors are alternatives to Stripe, the sheer popularity of their products for online payments make them an easy choice.

It's common for SaaS companies to handle recurring subscriptions, but many products would benefit from one-time sales of digital products. This is equally as easy with Stripe's infrastructure, but their documentation seems to shy away from the topic.

## Setup

To explain the process, we'll build an application that allows users to pay to post an advertisement on a craigslist-style board. For our specific example, our application will be a website for people looking for roommates. Users can browse the board for free but must pay a fee to post their own advertisement.

### Create a Stripe Account

To begin, you'll need to head on over to [Stripe's website](https://stripe.com/) to create an account. Sign up with your information or the information of your business if applicable.

![A screenshot of the Stripe sign-up page](https://www.honeybadger.io/images/blog/posts/sell-a-one-time-purchase-in-rails-with-stripe/stripe-signup.png "Stripe sign-up page")
_A screenshot of the Stripe sign-up page_

This article won't walk you through the exact details of creating an account, but [their documentation](https://support.stripe.com/questions/getting-started-with-stripe-create-or-connect-an-account) will likely answer any questions you have. We're particularly interested in their **payments and payouts** product or, more specifically, **Stripe Checkout**.

### Create a Basic Rails App

Once your Stripe account is set up, it's time to create a basic Rails app that we'll use for the integration. If you are integrating an existing app or are just interested in the Stripe part of the tutorial, skip ahead to "Basic Stripe Integration".

I'll use the following versions for this example:

- Rails 6.1
- Ruby 3.0.0

Assuming you have both Ruby and Rails already installed, go ahead and run the following:

```bash
rails new roommate-board
```

The name I've chosen for the app is `roommate-board`, but you're free to choose another. Just swap out the name anywhere else you see it mentioned in the example code.

Change into the directory that was just created with the following:

```bash
cd roommate-board
```

Then, run the app with the following:

```bash
rails s
```

If you see the typical Rails welcome page when you visit `localhost:3000`, then congrats, we're ready to get coding!

First, we'll add Devise to handle user authentication for us. Simply add `gem 'devise'` to your Gemfile and then run the following:

```bash
bundle install
```

Next, use the Devise generator to set things up by running the following:

```bash
rails generate devise:install
```

Next, connect Devise to a User model by running the following:

```bash
rails generate devise User
```

And finally, run the migration with the following:

```bash
rails db:migrate
```

### Create a Roommate Board

If you're reading this, you're probably more interested in Stripe integration than front-end design, so we won't worry about styling our example.

Our next step is to **create the model for posting**! We'll use Rails' handy generators to create the model, migration, controller, and routes. Just run the following:

```bash
rails generate scaffold Post address:string rent:integer content:text
```

This model doesn't contain everything we'll need to integrate Stripe. We'll add it later in case you're referencing this tutorial for a project that you don't have the luxury of starting from scratch. Run the migration to create the database table with the following:

```bash
rails db:migrate
```

Next, we'll **make the index view for posts the root of our application** for convenience. In `config/routes.rb`, add the following line:

```ruby
root 'posts#index'
```

It's pretty empty right now, and there's no clear way to sign in, sign out, or sign up. The functionality is already there, so let's just add some links in a header to make it clear to the user. In `app/views/layouts/application.html.erb`, we will add the necessary links. My file looks like this after making the changes:

```erb
<!DOCTYPE html>
<html>
  <head>
    <title>RoommateBoard</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header>
      <nav>
        <a href="/#" class="block">
          <h1>Roommate Board</h1>
        </a>
        <% if user_signed_in? %>
          <%= link_to "Manage Posts", manage_posts_url %>
          <%= link_to "Sign Out", destroy_user_session_url, :method => 'delete' %>
        <% else %>
          <%= link_to "New Post", new_user_registration_url %>
        <% end %>
      </nav>
    </header>
    <%= yield %>
  </body>
</html>
```

This gives a link to the homepage, and when users click "New Post", they'll be taken to the sign-up page. If they're signed in, it changes to a "Manage Posts" button and a "Sign Out" button. Without any styling and without being signed in, the root page now looks like this:

![A screenshot of the basic posts index page](https://www.honeybadger.io/images/blog/posts/sell-a-one-time-purchase-in-rails-with-stripe/basic-index.png "Posts index page")
_A screenshot of the basic posts index page_

Next, we want to **scope each posting to a unique user**. Essentially, we'll be creating a belongs_to association between a Posting and User, along with some relevant logic. We'll start with Rails’ handy migration generator; just run the following:

```bash
rails g migration AddUserIdToPosts
```

Now edit the migration file in `db/migrate/` to look like this:

```ruby
class AddUserIdToPosts < ActiveRecord::Migration[6.1]
  def change
    add_column :posts, :user_id, :integer
  end
end
```

Finally, run the migration with the following:

```bash
rails db migrate
```

Next, we'll **add associations to the models themselves**. In `app/models/post.rb`, add the following line:

```ruby
belongs_to :user
```

And in `app/models/user.rb`, add the following:

```ruby
has_many :posts
```

This bidirectional association gives us some handy Rails methods, like the ones we're about to use in the create and new methods. Navigate to `app/controllers/posts_controller.rb` (the posts controller) and take a look at the _new_ method. Instead of `@post = Post.new`, use `@post = current_user.posts.build`.

In the _create_ method, we'll do something similar. Replace `@post = Post.new(post_params)` with `@post = current_user.posts.build(post_params)`. Together, the new and create methods should look like this:

```ruby
# GET /posts/new
def new
  @post = current_user.posts.build
end

  # POST /posts or /posts.json
def create
  @post = current_user.posts.build(post_params)

  respond_to do |format|
    if @post.save
      format.html { redirect_to @post, notice: "Post was successfully created." }
      format.json { render :show, status: :created, location: @post }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end
```

This ensures that the user's id attribute is stored in every post, but it doesn't stop another user from editing or deleting a post that isn't theirs! In the same PostsController, let's write a method that gives us a Boolean indicator of whether a user owns a posting. Add this to the bottom of the controller:

```ruby
def user_owns_post?
  @post.user == current_user
end
```

In the **edit** method, add the following (it will be the only code in there):

```ruby
unless user_owns_post?
  # Redirect them to an error page
  redirect_to posts_path, flash: { error: "You are not the owner of that post!" }
end
```

In the **destroy** method, add this to the beginning:

```ruby
unless user_owns_post?
  # Redirect them to an error page
  redirect_to posts_path, flash: { error: "You are not the owner of that post!" }
end
```

Next, we'll need some functionality for a user to **manage their existing posts**. The end goal is for a user to be able to create a post and have it in a draft state before they pay. Once they pay, it is set to active for them! In the PostsController, add a new method:

```ruby
def manage
  @posts = current_user.posts
end
```

Next, add a route so that the user can get there! In `config/routes.rb`, add the following:

```ruby
get "/manage_posts/" =>'posts#manage'
```

Finally, we'll create the view for the post management! Add a new file, `app/views/posts/manage.html.erb`. Then, copy the entire contents of `app/views/posts/index.html.erb` into it. Simply change the header from "Posts" to "Manage Posts". This is essentially an index page, but with an individual user's posts being the only ones indexed thanks to our controller logic. Technically, we could do this on the individual index page, but this separation makes it easier to add other functionalities later. For example, we'll now **add a link to create a new post** on this management page.

Under the header, simply add the following:

```ruby
<%= link_to "New Post", new_post_url%>
```

This is all the setup we needed! It's a good idea to go through it to make sure everything is working properly. You should now be able to do the following:

- Create a user account
- Sign in
- Sign out
- View an index of existing postings
- Create a new posting

If it's all working the way you expect, then it's time to move on to Stripe integration!

## Stripe Integration

### Adding the Stripe Ruby Gem

To begin, we'll just add the Stripe Ruby gem. In your Gemfile, add the following line:

```ruby
gem 'stripe'
```

Follow that with running:

```bash
bundle install
```

For the next part, you'll need your **Stripe API key**. On the Stripe console, click "Developer" on the sidebar. An option for "API Keys" will reveal itself, so go ahead and click that next. Take a note of your "Secret Key," as we'll need it shortly.

But first, create an initializer in `config/initializers`. Create one called `stripe.rb`. In this file, add a single line that looks like this (but with your secret key substituted):

```ruby
Stripe.api_key = <insert your key here as a string>
```

### Storing Stripe Credentials

This method of storing our credentials is insecure, however. We shouldn't store production secrets in plaintext, where they are tracked by git and accessible to anyone reading the codebase. Fortunately, Rails provides a way to securely store credentials.

In your shell, run `EDITOR=vim bin/rails credentials:edit` to unencrypt and open the credentials file in vim. Hit the 'i' key on your keyboard to switch to insert mode. Add a new section that looks like this:

```yml
stripe:
  secret: your-secret-key
  public: your-public-key
```

Next, save the file and exit vim. There are a few ways to do this, but my favorite is by hitting the escape key (to leave insert mode) and then typing `:wq`, followed by the enter key. If you want to know more about how Rails handles encrypted credentials, [this is a good resource](https://blog.engineyard.com/rails-encrypted-credentials-on-rails-5.2).

Now that the credentials are safely stored, swap out the insecure initializer code with this:

```ruby
Stripe.api_key = Rails.application.credentials[:stripe][:secret]
```

### Linking to Stripe Checkout From the Product

There are a few different ways we can leverage Stripe for payments, but a great option is Stripe Checkout. Stripe Checkout is a _hosted payment page_, meaning you don't create any of the user interface for the actual transaction. A user will click a button, be redirected to Stripe, and then redirected back to your application after the payment goes through. This makes managing context a bit of a challenge, but it removes the burden of creating a payment form and lets Stripe do a lot of work to maximize conversions. You can easily accept payment types beyond cards, including ACH transactions or many international payment methods.

To begin, **create a controller to handle the checkout process**. Create `app/controllers/checkout_controller.rb`. In it, we'll write a method to handle the creation of a checkout session.

```ruby
def create
  @session = Stripe::Checkout::Session.create({
    success_url: root_url,
    cancel_url: manage_posts_url,
    payment_method_types: ['card'],
    line_items: [{
        name: "Roommate Posting",
        amount: 2000,
        currency: "usd",
        quantity: 1
    }],
    mode: 'payment',
    metadata: {post_id: params[:post_id]},
    customer_email: current_user.email,
    success_url: manage_posts_url,
    cancel_url: manage_posts_url
  })

  respond_to do |format|
    format.js
  end
end
```

This creates one Checkout Session with the necessary context for Stripe to do its magic. We pass along a single line item, which we hard-coded here since our application only has one product. But this would be an easy place to use a passed parameter to customize the checkout for a particular product. The "Amount" variable is the cost of the product _in cents_. It's also worth mentioning that we pass in the current user's email for simplicity's sake. The last thing that's important to call out is that we pass along the id of a post as _metadata_. This will make it easy for us to fulfill our user's purchase in the web hooks section!

Next, we'll obviously need a way for the method to be called.

In `config/routes.rb`, add the following line to create a route to the controller.

```ruby
post 'checkout/create' => 'checkout#create', as: "checkout_create"
```

Next, we'll **add a button to submit payment** for any post in the manage posts view. In `app/views/manage.html.erb`, add an extra column to the header by adding this last:

```html
<th>Payment</th>
```

Also, switch `<th colspan="3"></th>` to `<th colspan="4"></th>`.

Next, add another item to the table body for the payment button itself. As the fourth item in the body, add the following:

```html
<td><%= button_to "Submit Payment", checkout_create_path, params: {:post_id => post.id }, remote: true %></td>
```

All in all, the manage posts view now looks like this:

```ruby
<p id="notice"><%= notice %></p>

<h1>Manage Posts</h1>
<%= link_to "New Post", new_post_url%>

<table>
  <thead>
    <tr>
      <th>Address</th>
      <th>Rent</th>
      <th>Content</th>
      <th>Payment</th>
      <th colspan="4"></th>
    </tr>
  </thead>

  <tbody>
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.address %></td>
        <td><%= post.rent %></td>
        <td><%= post.content %></td>
        <td><%= button_to "Submit Payment", checkout_create_path, params: {:post_id => post.id }, remote: true %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Post', new_post_path %>
```

### JavaScript

Don't forget to add the Stripe JavaScript package include to `app/views/application.html.erb` with this line:

```html
<script src="https://js.stripe.com/v3/"></script>
```

Next, you'll need to add a new file (and directory!) in `app/views/checkout/create.js.erb`. In this file, just add the following to allow the checkout session to be created upon clicking the button with the help of the controller.

```javascript
var stripe = Stripe("<%= Rails.application.credentials[:stripe][:public] %>")

stripe.redirectToCheckout({
    sessionId: '<%= @session.id %>'
}).then(function (result) {
    console.log(result.error_message)
});
```

### Setting Up Webhooks

Now, we have a way for users to pay for posts! However, we don't have a way for the application to know whether a post is paid for or activate it when it is. To start, add a Boolean to the post model to indicate whether a post has been paid for. To use the Rails migration generator, run the following:

```bash
rails g migration AddPaymentDetailsToPost
```

Open up this migration and add the following line to it:

```ruby
add_column :posts, :is_paid, :boolean, :default => false
```

This adds an attribute/column to the posts table/model called `is_paid`. This attribute is a Boolean that defaults to false, which means that whenever a post is created, it is marked as having _not been paid for_. When a post is paid for, we'll manually flip the Boolean. But first, run the migration you just wrote with:

```bash
rails db:migrate
```

Because payments don't instantly process, we can't rely on a successful API response from Stripe to determine that a job has been paid for. Instead, we can indicate an endpoint on our application for Stripe to make a post request when a payment finishes processing. This process is generally referred to as webhooks and is actually simpler than it sounds!

To start, create a new controller in `app/controllers/` called WebhooksController. In this `app/controllers/webhooks_controller.rb`, write the following:

```ruby
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    payload = request.body.read
    sig_header = request.env['HTTP_STRIPE_SIGNATURE']
    event = nil

    begin
    event = Stripe::Webhook.construct_event(
      payload, sig_header, Rails.application.credentials[:stripe][:webhook]
    )
    rescue JSON::ParserError => e
      status 400
    return
    rescue Stripe::SignatureVerificationError => e
      # Invalid signature
      puts "Signature error"
      p e
      return
    end

    # Handle the event
    case event.type
    when 'checkout.session.completed'
      session = event.data.object
      post = Post.find_by(id: session.metadata.post_id)]
      post.is_paid = true
      post.save!
    end

    render json: { message: 'success' }
  end
end 
```

Other than the necessary boilerplate, this method will hand a specific event type, called `checkout.session.completed`. All that's left on the Rails side of things is to add the route to `config/routes.rb`:

```ruby
resources :webhooks, only: [:create]
```

Finally, you'll need to connect your Stripe account to this endpoint in the Stripe dashboard itself. Because you'll be supplying a URL to the Stripe dashboard, this will not work locally! You must deploy this to an internet-accessible endpoint for webhooks to function properly.

Return to "Developers" on the Stripe dashboard , but this time, select "Webhooks" on the left panel. In the "Endpoints" section, click "Add Endpoint". Supply your applications URL appended with `/webhooks` and assign it the event "checkout.session.completed".

And that's it for Stripe integration! The last practical step for our mock application is to have the index page only display posts that have been paid for. This can easily be done in the index method of `app/controllers/posts_controller.rb`. Change the method to this:

```ruby
def index
  @posts = Post.where(:is_paid => true)
end
```

Now, users can create a post, pay for it with Stripe checkout, and have it automatically displayed on the application's homepage!

### Switching to Production

Because your webhooks only work when deployed to a live site, it's worthwhile to discuss changes needed for deployment. For starters, your communication **must** be HTTPS for Stripe checkout to work. If you're experiencing any issues, check the JavaScript console for some hints from Stripe.

There's a good chance that in your initial setup, you used Stripe's test keys. If you did, the API Keys section of the Stripe dashboard will look something like this.

![A screenshot of Stripe dashboard API Keys](https://www.honeybadger.io/images/blog/posts/sell-a-one-time-purchase-in-rails-with-stripe/api-keys.png "Stripe Dashboard API Keys Section")
_A screenshot of Stripe dashboard API Keys_

Simply click the toggle next to "viewing test data" to reveal your production secret and public keys. You'll need to open the Rails credentials manager in the same way as before and replace the test keys with this for your application to process live data. The test environment is helpful, however, for running test transactions with fake credit card numbers. If you have more than one environment or plan on switching this often, it's worthwhile to create another key/value pair in the credentials manager and dynamically use it in your code.

## Conclusion

We've spent a lot of time creating our example application and not too long integrating Stripe. Thanks to Stripe checkout, we pass off a lot of responsibility, including the user interface, saving us plenty of code. This creates a consistent experience for users, given Stripe's widespread adoption in the online marketplace. Stripe's mission is to increase the GDP of the internet, and they need applications like yours to do that. Because of this, they're always looking for _even easier_ ways for you to process payments. While Stripe Checkout is fairly easy to implement, they've recently thought of an even faster way.

[Stripe Payment Links](https://stripe.com/payments/payment-links) is a brand-new product from Stripe that has the potential to make our Checkout integration almost unnecessary. All we really did was redirect the user to a hosted checkout page, and a simple link has the potential to make that even easier. Payment links are a no-code solution that seems to be directed at retailers but could potentially work for this use case. Regardless, Stripe Checkout still boasts a bit more flexibility and adoption in the marketplace, so it's important to fully understand.

One thing many businesses struggle with is handling taxes, especially internationally. Stripe recently [introduced a new feature](https://stripe.com/docs/tax) to automatically handle this for businesses, but it's still in beta. Once available, it should greatly reduce the accounting burden of anyone using Stripe for payment processing!

Stripe's documentation and existing articles directed at Ruby on Rails lean towards subscriptions rather than one-time purchases. Subscriptions are recurring charges attached to a user and a product. On Stripe's end, the implementation looks a lot like one-time purchases. On the application side, however, we would have to integrate subscription management, allow the user to manage their subscription, and periodically check subscription status. This article focused on one-time purchases, but there's plenty of resources in the community should you need to manage subscriptions.