😖 I really dislike Stripe Elements: you have to build and style the checkout form yourself.

stripe-checkout-elements.png

🤩 Instead, stripe-hosted full screen checkout is a much better approach where you outsource all the payment logic to Stripe. It looks great:

stripe-checkout-hosted.gif

Heres a Github Repo where I have it implemented.

Stripe Hosted Checkout on SupeRails:

stripe-checkout-hosted-superails.gif

🥰🥰 But now you can use Embedded Stripe Checkout inside your app and keep your branding!

stripe-checkout-embedded-superails.gif

Here’s how you can implement Embedded Stripe Checkout: #

Create a pricing page that redirects to the checkout page

# config/routes.rb
  get "pricing", to: "stripe/checkout#pricing"
  get "checkout/new", to: "stripe/checkout#checkout", as: "new_checkout"

Stripe Embedded Checkout API has ui_mode: :embedded and return_url params that you should set:

# app/controllers/stripe/checkout_controller.rb
  # GET
  def pricing
    lookup_key = %w[month year lifetime]
    @prices = Stripe::Price.list(lookup_keys: lookup_key, active: true,
                                 expand: ['data.product']).data.sort_by(&:unit_amount)
  end

  # GET
  def checkout
    price = Stripe::Price.retrieve(params[:price_id])
    @session = Stripe::Checkout::Session.create({
                                                  customer: current_user.stripe_customer_id,
                                                  # allow_promotion_codes: true,
                                                  # automatic_tax: {enabled: @plan.taxed?},
                                                  # consent_collection: {terms_of_service: :required},
                                                  # customer_update: {address: :auto},
                                                  # payment_method_collection: :if_required,
                                                  line_items: [{
                                                    price:,
                                                    quantity: 1
                                                  }],
                                                  mode: mode(price),
                                                  return_url: user_url(current_user),
                                                  ui_mode: :embedded
                                                })
  end

  private

  MODES = {
    'recurring' => 'subscription',
    'one_time' => 'payment',
    'setup' => 'setup'
  }.freeze

  def mode(price)
    MODES[price.type]
  end

Display links to checkout for each different price:

# app/views/stripe/checkout/pricing.html.erb
<% @prices.each do |price| %>
  <%= link_to "Checkout" new_checkout_path(price_id: price.id) %>
<% end %>

This will redirect to the checkout page. You will need some JS to embed the Stripe Checkout.

# app/views/stripe/checkout/checkout.html.erb
<%= javascript_include_tag "https://js.stripe.com/v3/" %>

<%= tag.div data: {
  controller: "stripe--embedded-checkout",
  stripe__embedded_checkout_public_key_value: Rails.application.credentials.dig(Rails.env, :stripe, :public),
  stripe__embedded_checkout_client_secret_value: @session.client_secret
} %>
rails g stimulus stripe/embedded_checkout
// app/javascript/controllers/stripe/embedded_checkout_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    publicKey: String,
    clientSecret: String,
  }

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

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

That’s it! Now you have the latest, coolest Stripe Checkout!

Don’t forget to also add:

  • Webhooks to create customers, handle successful and failed payments, subscription state changes
  • Billing portal for user to manage plan, change payment methods, see invoice history

See examples here: github.com/corsego/rails-7-stripe-subscriptions