Skip to content

Add mobile in-app purchases (IAP) to your Rails app with PurchaseKit and Pay.

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
MIT-LICENSE
Notifications You must be signed in to change notification settings

PurchaseKit/purchasekit

Repository files navigation

PurchaseKit

In-app purchase webhooks for Rails. Receive normalized Apple and Google subscription events with a simple callback interface.

How it works

Native app (iOS/Android)
    ↓ StoreKit/Play Billing
App Store / Play Store
    ↓ Server-to-server notifications
PurchaseKit SaaS (normalizes Apple/Google data)
    ↓ Webhooks
Your Rails app (via this gem)
    ↓ Callbacks or Pay::Subscription
Your business logic

PurchaseKit handles the complexity of Apple and Google's different webhook formats, delivering you a consistent event payload regardless of which store the purchase came from.

Installation

Add to your Gemfile:

gem "purchasekit"

Create an initializer:

# config/initializers/purchasekit.rb
PurchaseKit.configure do |config|
  config.api_key = Rails.application.credentials.dig(:purchasekit, :api_key)
  config.app_id = Rails.application.credentials.dig(:purchasekit, :app_id)
  config.webhook_secret = Rails.application.credentials.dig(:purchasekit, :webhook_secret)
end

Mount the engine in your routes:

# config/routes.rb
mount PurchaseKit::Engine, at: "/purchasekit"

Import the JavaScript:

// app/javascript/application.js
import "purchasekit/turbo_actions"

// app/javascript/controllers/index.js
eagerLoadControllersFrom("purchasekit", application)

Pay gem integration

If you use the Pay gem, PurchaseKit automatically detects it and handles everything:

gem "pay"
gem "purchasekit"

When Pay is detected, webhooks automatically create and update Pay::Subscription records and broadcast Turbo Stream redirects. No event callbacks needed.

Event callbacks (without Pay)

If you're not using Pay, register callbacks to handle subscription events:

# config/initializers/purchasekit.rb
PurchaseKit.configure do |config|
  # ... credentials ...

  config.on(:subscription_created) do |event|
    user = User.find(event.customer_id)
    user.subscriptions.create!(
      processor_id: event.subscription_id,
      store: event.store,
      status: event.status
    )
  end

  config.on(:subscription_canceled) do |event|
    subscription = Subscription.find_by(processor_id: event.subscription_id)
    subscription&.update!(status: "canceled")
  end

  config.on(:subscription_expired) do |event|
    subscription = Subscription.find_by(processor_id: event.subscription_id)
    subscription&.update!(status: "expired")
  end
end

Available events

Event Description
:subscription_created New subscription started
:subscription_updated Subscription renewed or plan changed
:subscription_canceled User canceled (still active until ends_at)
:subscription_expired Subscription ended

Event payload

Method Description
event.event_id Unique event identifier (for idempotency)
event.customer_id Your user ID
event.subscription_id Store's subscription ID
event.store "apple" or "google"
event.store_product_id e.g., "com.example.pro.annual"
event.status "active", "canceled", "expired"
event.current_period_start Start of billing period
event.current_period_end End of billing period
event.ends_at When subscription will end
event.success_path Redirect path after purchase

Idempotency

Webhooks may be delivered more than once. Write idempotent callbacks using find_or_create_by or check event.event_id to avoid duplicate side effects.

Paywall helper

Build a paywall using the included helper. Subscribe to the Turbo Stream for real-time redirects:

<%= turbo_stream_from "purchasekit_customer_#{current_user.id}" %>

<%= purchasekit_paywall customer_id: current_user.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 %>

The restore link checks your server for an active subscription. Implement the endpoint in your app:

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

# 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

Products are fetched from the PurchaseKit API:

@annual = PurchaseKit::Product.find("prod_XXXXXXXX")
@monthly = PurchaseKit::Product.find("prod_YYYYYYYY")

Demo mode

For local development without a PurchaseKit account:

PurchaseKit.configure do |config|
  config.demo_mode = true
  config.demo_products = {
    "prod_annual" => { apple_product_id: "com.example.pro.annual" },
    "prod_monthly" => { apple_product_id: "com.example.pro.monthly" }
  }
end

Works with Xcode's StoreKit local testing.

License

MIT License. See LICENSE for details.

About

Add mobile in-app purchases (IAP) to your Rails app with PurchaseKit and Pay.

Resources

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
MIT-LICENSE

Stars

Watchers

Forks

Packages

No packages published