Paywall

Build an HTML paywall that triggers native in-app purchases.

Create a paywall

Fetch products in your controller:

class PaywallsController < ApplicationController
  def show
    @annual = PurchaseKit::Product.find("prod_XXXXXXXX")
    @monthly = PurchaseKit::Product.find("prod_YYYYYYYY")
  end
end

You'll find product IDs in the PurchaseKit dashboard under Apps → [Your App] → Products.

Minimal example

<%= turbo_stream_from current_user.payment_processor %>

<%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %>
  <%= paywall.plan_option product: @annual, selected: true do %>
    Annual - <%= paywall.price %>/year
  <% end %>

  <%= paywall.plan_option product: @monthly do %>
    Monthly - <%= paywall.price %>/month
  <% end %>

  <%= paywall.submit "Subscribe" %>
<% end %>

<%= button_to "Restore purchases", restore_purchases_path %>

Styled example (Bootstrap)

<%= turbo_stream_from current_user.payment_processor %>

<%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %>
  <div class="d-flex flex-column gap-2 mb-3">
    <%= paywall.plan_option product: @annual, selected: true, class: "btn btn-outline-primary text-start" do %>
      <strong>Annual</strong>
      <span class="text-muted"><%= paywall.price %>/year</span>
    <% end %>

    <%= paywall.plan_option product: @monthly, class: "btn btn-outline-primary text-start" do %>
      <strong>Monthly</strong>
      <span class="text-muted"><%= paywall.price %>/month</span>
    <% end %>
  </div>

  <%= paywall.submit "Subscribe", class: "btn btn-primary w-100" %>
  <div class="text-center mt-2">
    <%= button_to "Restore purchases", restore_purchases_path, class: "btn btn-link link-secondary small p-0" %>
  </div>
<% end %>

Paywall helper options

Option Description
customer_id: Required. Your Pay::Customer ID (use current_user.payment_processor.id)
success_path: Where to redirect after purchase (defaults to root_path)

Builder methods

Method Description
plan_option(product:, selected:) Radio button and label for a plan
price Localized price (must be inside plan_option block)
submit(text) Submit button (disabled until prices load)

Restore purchases

Add a restore link that checks your server for an active subscription:

# config/routes.rb
post "restore_purchases", to: "subscriptions#restore"

# app/controllers/subscriptions_controller.rb
def restore
  if current_user.subscribed?
    redirect_to dashboard_path, notice: "Your subscription is active."
  else
    redirect_to paywall_path, alert: "No active subscription found."
  end
end

Webhook events

The gem handles these events automatically:

Event Action
subscription.created Creates Pay::Subscription, redirects user
subscription.updated Updates status and period dates
subscription.canceled Marks subscription as canceled
subscription.expired Marks subscription as expired

Next step

Add the native package to your Hotwire Native app: