Subscriptions are handled differently by each payment processor. Pay does its best to treat them the same.
Pay stores subscriptions in the Pay::Subscription
model. Each subscription has a name
that you can use to handle multiple subscriptions per customer.
To subscribe a user, you can call the subscribe
method.
@user.payment_processor.subscribe(name: "default", plan: "monthly")
You can pass additional options to go directly to the payment processor's API. For example, the quantity
option to subscribe to a plan with per-seat pricing.
@user.payment_processor.subscribe(name: "default", plan: "monthly", quantity: 3)
Subscribe takes several arguments and options:
name
- A name for the subscription that's used internally. This allows a customer to have multiple subscriptions. Defaults to"default"
plan
- The Plan or Price ID to subscribe to. Defaults to"default"
quantity
- The quantity of the subscription. Defaults to1
trial_period_days
- Number of days for the subscription's trial.- Other options may be passed and will be sent directly to the payment processor's API.
Paddle does not allow you to create a subscription through the API.
Instead, Pay uses webhooks to create the the subscription in the database. The Paddle passthrough parameter is required during checkout to associate the subscription with the correct Pay::Customer
.
In your Javascript, include passthrough
in Checkout using the Pay::PaddleClassic.passthrough
helper.
Paddle.Checkout.open({
product: 12345,
passthrough: "<%= Pay::PaddleClassic.passthrough(owner: current_user) %>"
});
Or with Paddle Button Checkout:
<a href="#!" class="paddle_button" data-product="12345" data-email="<%= current_user.email %>" data-passthrough="<%= Pay::PaddleClassic.passthrough(owner: current_user) %>">Buy now!</a>
Pay provides a helper method for generating the passthrough
JSON object to associate the purchase with the correct Rails model.
Pay::PaddleClassic.passthrough(owner: current_user, foo: :bar)
#=> { owner_sgid: "xxxxxxxx", foo: "bar" }
# To generate manually without the helper
#=> { owner_sgid: current_user.to_sgid.to_s, foo: "bar" }.to_json
Pay uses a signed GlobalID to ensure that the subscription cannot be tampered with.
When processing Paddle webhooks, Pay parses the passthrough
JSON string and verifies the owner_sgid
hash in order to find the correct Pay::Customer
record.
The passthrough parameter owner_sgid
is only required for creating a subscription.
As with Paddle Classic, Paddle Billing does not allow you to create a subscription through the API.
Instead, Pay uses webhooks to create the the subscription in the database. The Paddle customer
field is required
during checkout to associate the subscription with the correct Pay::Customer
.
Firstly, retrieve/create a Paddle customer by calling customer
.
@user.payment_processor.customer
Then using either the Javascript Paddle.Checkout.open
method or the Paddle Button Checkout, pass the customer
object
and an array of items to subscribe to.
Paddle.Checkout.open({
customer: {
id: "<%= @user.payment_processor.processor_id %>",
},
items: [
{
// The Price ID of the subscription plan
priceId: "pri_abc123",
quantity: 1
}
],
});
Or with Paddle Button Checkout:
<a href='#'
class='paddle_button'
data-display-mode='overlay'
data-locale='en'
data-items='[
{
"priceId": "pri_abc123",
"quantity": 1
}
]'
data-customer-id="<%= @user.payment_processor.processor_id %>"
>
Subscribe
</a>
Lemon Squeezy does not allow you to create a subscription through the API. Instead, Pay uses webhooks to create the subscription in the database.
Lemon Squeezy offer 2 checkout flows, a hosted checkout and a checkout overlay. When creating a Product in the Lemon Squeezy dashboard, clicking the "Share" button will provide you with the URLs for either checkout flow.
For example, the hosted checkout flow:
https://STORE.lemonsqueezy.com/checkout/buy/UUID
And the checkout overlay flow:
<a href="https://STORE.lemonsqueezy.com/checkout/buy/UUID?embed=1" class="lemonsqueezy-button">Buy A Product</a>
<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
It's currently not possible to pass a pre-existing Customer ID to Lemon Squeezy, so you can use the passthrough
method to associate the subscription with the correct Pay::Customer
.
You can pass additional options to the checkout session. You can view the supported fields here and the custom data field here.
You can use the Pay::LemonSqueezy.passthrough
helper to generate the checkout[custom][passthrough]
field.
You'll need to replace storename
with your store URL slug & UUID
with the UUID of the plan you want to use, which
can be found by clicking Share on the product in Lemon Squeezy's dashboard.
<a
class="lemonsqueezy-button"
href="https://storename.lemonsqueezy.com/checkout/buy/UUID?checkout[custom][passthrough]=<%= Pay::LemonSqueezy.passthrough(owner: @user) %>">
Sign up to Plan
</a>
@user.payment_processor.subscription(name: "default")
There are two types of trials for subscriptions: with or without a payment method upfront.
Stripe is the only payment processor that allows subscriptions without a payment method. Braintree and Paddle require a payment method on file to create a subscription.
To create a trial without a card, we can use the Fake Processor to create a subscription with matching trial and end times.
time = 14.days.from_now
@user.set_payment_processor :fake_processor, allow_fake: true
@user.payment_processor.subscribe(trial_ends_at: time, ends_at: time)
This will create a fake subscription in our database that we can use. Once expired, the customer will need to subscribe using a real payment processor.
@user.payment_processor.on_generic_trial?
#=> true
Braintree and Paddle require payment methods before creating a subscription.
@user.set_payment_processor :braintree
@user.payment_processor.payment_method_token = params[:payment_method_token]
@user.payment_processor.subscribe()
@user.payment_processor.subscribed?
You can also check for a specific subscription or plan:
@user.payment_processor.subscribed?(name: "default", processor_plan: "monthly")
You can check if the user is on a trial by simply asking:
@user.payment_processor.on_trial?
#=> true or false
You can also check if the user is on a trial for a specific subscription name or plan.
@user.payment_processor.on_trial?(name: 'default', plan: 'plan')
#=> true or false
For paid features of your app, you'll often want to check if the user is on trial OR subscribed. You can use this method to check both at once:
@user.payment_processor.on_trial_or_subscribed?
You can also check for a specific subscription or plan:
@user.payment_processor.on_trial_or_subscribed?(name: "default", processor_plan: "annual")
Individual subscriptions provide similar helper methods to check their state.
@user.payment_processor.subscription.on_trial? #=> true or false
@user.payment_processor.subscription.cancelled? #=> true or false
@user.payment_processor.subscription.on_grace_period? #=> true or false
@user.payment_processor.subscription.active? #=> true or false
@user.payment_processor.subscription.cancel
In addition to the API, Paddle provides a subscription Cancel URL that you can redirect customers to cancel their subscription.
@user.payment_processor.subscription.paddle_cancel_url
@user.payment_processor.subscription.cancel_now!
The subscription will be canceled immediately and you cannot resume the subscription.
If you wish to refund your customer for the remaining time, you will need to calculate that and issue a refund separately.
If a user wishes to change subscription plans, you can pass in the Plan or Price ID into the swap
method:
@user.payment_processor.subscription.swap("yearly")
Braintree does not allow this via their API, so we cancel and create a new subscription for you (including proration discount).
A user may wish to resume their canceled subscription during the grace period. You can resume a subscription with:
@user.payment_processor.subscription.resume
@user.payment_processor.subscription.processor_subscription
#=> #<Stripe::Subscription>
Stripe and Paddle allow you to pause subscriptions. These subscriptions are considered to be active. This allows the subscriptions to be displayed to your users so they can resume the subscription when ready. You will need to check if the subscription is paused if you wish to limit any feature access within your application.
@user.payment_processor.subscription.paused? #=> true or false
Stripe subscriptions have several behaviors.
behavior: void
will put the subscription on a grace period until the end of the current period.behavior: keep_as_draft
will pause the subscription invoices but the subscription is still active. Use this to delay payments until later.behavior: mark_uncollectible
will pause the subscription invoices but the subscription is still active. Use this to provide free access temporarily.
Calling pause with no arguments will set behavior: "mark_uncollectible"
by default.
@user.payment_processor.subscription.pause
You can set this to another option as shown below.
@user.payment_processor.subscription.pause(behavior: "mark_uncollectible")
@user.payment_processor.subscription.pause(behavior: "keep_as_draft")
@user.payment_processor.subscription.pause(behavior: "void")
@user.payment_processor.subscription.pause(behavior: "mark_uncollectible", resumes_at: 1.month.from_now)
Paddle will pause payments at the end of the period. The status remains active
until the period ends with a paused_from
value to denote when the subscription pause will take effect. When the status becomes paused
the subscription is no longer active.
@user.payment_processor.subscription.pause
@user.payment_processor.subscription.resume
In general, you don't need to use these methods as Pay's webhooks will keep you all your subscriptions in sync automatically.
However, for instance, a user returning from Stripe Checkout / Stripe Billing Portal might still see stale subscription information before the Webhook is processed, so these might come in handy.
@user.payment_processor.subscription.sync!
There's a convenience method for syncing all subscriptions at once (currently Stripe only).
@user.payment_processor.sync_subscriptions
As per Stripe's docs here, by default the list of subscriptions will not included canceled ones. You can, however, retrieve them like this:
@user.payment_processor.sync_subscriptions(status: "all")
Since subscriptions views are not frequently accessed by users, you might accept to trade off some latency for increased safety on these views, avoiding showing stale data. For instance, in your controller:
class SubscriptionsController < ApplicationController
def show
# This guarantees your user will always see up-to-date subscription info
# when returning from Stripe Checkout / Billing Portal, regardless of
# webhooks race conditions.
current_user.payment_processor.sync_subscriptions(status: "all")
end
def create
# Let's say your business model doesn't allow multiple subscriptions per
# user, and you want to make extra sure they are not already subscribed before showing the new subscription form.
current_user.payment_processor.sync_subscriptions(status: "all")
redirect_to subscription_path and return if current_user.payment_processor.subscription.active?
end
See Webhooks