Embedding Stripe Checkout

Episode #422 by Teacher's Avatar David Kimura

Summary

Stripe Checkout is one of my favorite ways to handle payments in Ruby on Rails applications. Stripe Checkouts will soon have an option to embed the Checkout into your web application. In this episode, we'll look at implementing this feature with a StimulusJS controller.
rails stripe stimulusjs hotwire 19:11

Chapters

  • Introduction (0:00)
  • Setting up the application (2:05)
  • Setting up Stripe (5:27)
  • Creating the Stripe Checkout Session (7:36)
  • Creating the stimulus controller (11:20)
  • Testing the Stripe Checkout (15:14)
  • Verifying the purchase (15:49)
  • Final thoughts (18:20)

Resources

Download Source Code

Summary

# Terminal
rails g scaffold product name 'price:decimal{8,2}'
rails g model order session_id stripe_checkout_id status:integer
rails g controller checkouts
rails g controller payments
bundle add stripe
bin/rails credentials:edit
rails g stimulus stripe

# credentials
stripe:
  publishable_key: pk_test_XXX
  secret_key: sk_test_XXX

# db/migrate/20231001004208_create_orders.rb
class CreateOrders < ActiveRecord::Migration[7.1]
  def change
    create_table :orders do |t|
      t.string :session_id
      t.string :stripe_checkout_id
      t.integer :status, default: 0

      t.timestamps
    end
  end
end

# app/models/order.rb
class Order < ApplicationRecord
  enum status: {
    pending: 0,
    paid: 1
  }
end

# config/routes.rb
resources :products
resource :checkout, only: :show
resource :payments, only: :show

# app/views/products/index.html.erb
<td><%= link_to "Purchase", checkout_path(id: product) %></td>

# config/initializers/stripe.rb
Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
Stripe.api_version = "2023-08-16;embedded_checkout_beta=v2"

# app/views/layouts/application.html.erb
<%= javascript_include_tag "https://js.stripe.com/v3/", "data-turbo-track": "reload" %>

# app/controllers/checkouts_controller.rb
class CheckoutsController < ApplicationController
  def show
    @session = Stripe::Checkout::Session.create(
      line_items: [{
        price_data: {
          currency: "usd",
          product_data: {
            name: product.name
          },
          unit_amount: (product.price * 100).to_i
        },
        quantity: 1
      }],
      mode: "payment",
      ui_mode: "embedded",
      return_url: CGI.unescape(payments_url(session_id: '{CHECKOUT_SESSION_ID}'))
    )

    Order.create(session_id: session.id, stripe_checkout_id: @session.id)
    # current_user.orders.create(stripe_checkout_id: @session.id)
  end

  private

  def product
    @product ||= Product.find(params[:id])
  end
end

# app/views/checkouts/show.html.erb
<div data-controller="stripe"
  data-stripe-public-key-value="<%= Rails.application.credentials.dig(:stripe, :publishable_key) %>"
  data-stripe-client-secret-value="<%= @session.client_secret %>">
</div>

# app/javascript/controllers/stripe_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="stripe"
export default class extends Controller {
  static values = { publicKey: String, clientSecret: String }
  stripe = Stripe(this.publicKeyValue, { betas: ["embedded_checkout_beta_1"] })

  async connect() {
    this.checkout = await this.stripe.initEmbeddedCheckout({
      clientSecret: this.clientSecretValue
    })
    this.checkout.mount(this.element)
  }

  disconnect() {
    this.checkout.destroy()
  }
}

# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
  def show
    @order = Order.find_by(
      session_id: session.id.to_s,
      stripe_checkout_id: params[:session_id]
    )
    stripe_session = Stripe::Checkout::Session.retrieve(params[:session_id])
    if stripe_session.status == "complete"
      @order.paid!
      # Other business logic
    # elsif stripe_session.status == "open"
    else
      @order.pending!
    end
  end
end

# app/views/payments/show.html.erb
<h1>Successful Payment</h1>

<%= @order.inspect %>