pay 12.1.0 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +3 -1
- data/app/controllers/pay/webhooks/paddle_billing_controller.rb +4 -1
- data/app/models/pay/paddle_billing/subscription.rb +22 -11
- data/app/models/pay/stripe/subscription.rb +36 -7
- data/app/views/pay/payments/show.html.erb +22 -11
- data/config/locales/en.yml +12 -0
- data/lib/pay/payment.rb +6 -0
- data/lib/pay/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19c2b44409eb406bf144f15e922e863e8265c0d65bba50141928da748ed0d0f0
|
|
4
|
+
data.tar.gz: c9dbd51994ab4747a672905bb3b6a0fadf9060ef46ebaaf5edd6acd3f4569197
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1ab458e90aaede5cdc5bb5418917c4084736bc4c48544b3d360ee4e0fac8975db1b24e363e218e2b2986837c7c97ea9724011b3196f00f9e85e9a7b0a26dce6
|
|
7
|
+
data.tar.gz: 744063befa0467573287d6264a87419018d63fd845a4cdd307f0cb0ecc08bb4570eb839524f9b225c3aeda0e7fdb1a1c095321e4090222c3f29d5355043e7d42
|
|
@@ -13,9 +13,11 @@ module Pay
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def valid_signature?(signature)
|
|
16
|
+
secret = Pay::LemonSqueezy.signing_secret
|
|
17
|
+
raise Pay::LemonSqueezy::Error, "Cannot verify signature without a Lemon Squeezy signing secret" if secret.blank?
|
|
16
18
|
return false if signature.blank?
|
|
17
19
|
|
|
18
|
-
hmac = OpenSSL::HMAC.hexdigest("sha256",
|
|
20
|
+
hmac = OpenSSL::HMAC.hexdigest("sha256", secret, request.raw_post)
|
|
19
21
|
ActiveSupport::SecurityUtils.secure_compare(hmac, signature)
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -14,11 +14,14 @@ module Pay
|
|
|
14
14
|
|
|
15
15
|
# The header looks like "ts=1671552777;h1=eb4d0dc8..."
|
|
16
16
|
def valid_signature?(signature)
|
|
17
|
+
secret = Pay::PaddleBilling.signing_secret
|
|
18
|
+
raise Pay::PaddleBilling::Error, "Cannot verify signature without a Paddle Billing signing secret" if secret.blank?
|
|
19
|
+
|
|
17
20
|
parts = signature.to_s.split(";").filter_map { |part| part.split("=", 2) if part.include?("=") }.to_h
|
|
18
21
|
ts, h1 = parts.values_at("ts", "h1")
|
|
19
22
|
return false if ts.blank? || h1.blank?
|
|
20
23
|
|
|
21
|
-
hmac = OpenSSL::HMAC.hexdigest("sha256",
|
|
24
|
+
hmac = OpenSSL::HMAC.hexdigest("sha256", secret, "#{ts}:#{request.raw_post}")
|
|
22
25
|
ActiveSupport::SecurityUtils.secure_compare(hmac, h1)
|
|
23
26
|
end
|
|
24
27
|
end
|
|
@@ -39,8 +39,6 @@ module Pay
|
|
|
39
39
|
pay_customer.payment_methods.destroy_all
|
|
40
40
|
when "trialing"
|
|
41
41
|
attributes[:trial_ends_at] = Time.parse(subscription.next_billed_at) if subscription.next_billed_at
|
|
42
|
-
when "paused"
|
|
43
|
-
attributes[:pause_starts_at] = Time.parse(subscription.paused_at) if subscription.paused_at
|
|
44
42
|
when "active", "past_due"
|
|
45
43
|
attributes[:trial_ends_at] = nil
|
|
46
44
|
attributes[:pause_starts_at] = nil
|
|
@@ -76,10 +74,12 @@ module Pay
|
|
|
76
74
|
end
|
|
77
75
|
|
|
78
76
|
# If a subscription is paused, cancel immediately
|
|
79
|
-
# Otherwise, cancel at period end
|
|
77
|
+
# Otherwise, cancel at period end. A scheduled pause is removed first
|
|
80
78
|
def cancel(**options)
|
|
81
79
|
return if canceled?
|
|
82
80
|
|
|
81
|
+
remove_scheduled_pause if will_pause?
|
|
82
|
+
|
|
83
83
|
response = ::Paddle::Subscription.cancel(
|
|
84
84
|
id: processor_id,
|
|
85
85
|
effective_from: options.fetch(:effective_from, paused? ? "immediately" : "next_billing_period")
|
|
@@ -94,8 +94,6 @@ module Pay
|
|
|
94
94
|
|
|
95
95
|
def cancel_now!(**options)
|
|
96
96
|
cancel(**options.merge(effective_from: "immediately"))
|
|
97
|
-
rescue ::Paddle::Error => e
|
|
98
|
-
raise Pay::PaddleBilling::Error, e
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
def change_quantity(quantity, **options)
|
|
@@ -117,16 +115,22 @@ module Pay
|
|
|
117
115
|
# A subscription could be set to cancel or pause in the future
|
|
118
116
|
# It is considered on grace period until the cancel or pause time begins
|
|
119
117
|
def on_grace_period?
|
|
120
|
-
(canceled? &&
|
|
118
|
+
(canceled? && ends_at.future?) || will_pause?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def will_pause?
|
|
122
|
+
pause_starts_at? && pause_starts_at.future?
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
# Paddle keeps the status `active` until a scheduled pause starts, so check pause_starts_at as well
|
|
123
126
|
def paused?
|
|
124
|
-
status == "paused"
|
|
127
|
+
status == "paused" || pause_starts_at?
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
# Pauses the subscription at the end of the current billing period
|
|
127
131
|
def pause
|
|
128
132
|
response = ::Paddle::Subscription.pause(id: processor_id)
|
|
129
|
-
update!(status:
|
|
133
|
+
update!(status: response.status, pause_starts_at: response.scheduled_change.effective_at)
|
|
130
134
|
rescue ::Paddle::Error => e
|
|
131
135
|
raise Pay::PaddleBilling::Error, e
|
|
132
136
|
end
|
|
@@ -141,9 +145,8 @@ module Pay
|
|
|
141
145
|
end
|
|
142
146
|
|
|
143
147
|
# Paddle Billing API only allows "resuming" subscriptions when they are paused
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
|
|
148
|
+
if will_pause?
|
|
149
|
+
remove_scheduled_pause
|
|
147
150
|
else
|
|
148
151
|
::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
|
|
149
152
|
end
|
|
@@ -168,6 +171,14 @@ module Pay
|
|
|
168
171
|
)
|
|
169
172
|
update(processor_plan: plan, ends_at: nil, status: :active)
|
|
170
173
|
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# Paddle rejects changes to a subscription with a scheduled change
|
|
178
|
+
def remove_scheduled_pause
|
|
179
|
+
::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
|
|
180
|
+
update!(pause_starts_at: nil)
|
|
181
|
+
end
|
|
171
182
|
end
|
|
172
183
|
end
|
|
173
184
|
end
|
|
@@ -217,7 +217,7 @@ module Pay
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def paused?
|
|
220
|
-
|
|
220
|
+
status_paused? || pause_behavior_void?
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
# Pauses a Stripe subscription
|
|
@@ -266,12 +266,22 @@ module Pay
|
|
|
266
266
|
on_grace_period? || paused?
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
# Resumes a subscription that is on its grace period or paused
|
|
270
|
+
#
|
|
271
|
+
# Subscriptions with a `paused` status are billed immediately and stay paused until that invoice is paid
|
|
272
|
+
# Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if that payment needs a payment method or authentication
|
|
273
|
+
#
|
|
274
|
+
# resume(billing_cycle_anchor: "unchanged")
|
|
275
|
+
#
|
|
276
|
+
# https://docs.stripe.com/api/subscriptions/resume
|
|
277
|
+
def resume(**options)
|
|
270
278
|
unless resumable?
|
|
271
|
-
raise Error, "You can only resume subscriptions within their grace period."
|
|
279
|
+
raise Error, "You can only resume subscriptions that are paused or within their grace period."
|
|
272
280
|
end
|
|
273
281
|
|
|
274
|
-
if
|
|
282
|
+
if status_paused?
|
|
283
|
+
@api_record = ::Stripe::Subscription.resume(processor_id, options.merge(expand_options), stripe_options)
|
|
284
|
+
elsif paused?
|
|
275
285
|
unpause
|
|
276
286
|
else
|
|
277
287
|
@api_record = ::Stripe::Subscription.update(processor_id, {
|
|
@@ -281,6 +291,10 @@ module Pay
|
|
|
281
291
|
stripe_options)
|
|
282
292
|
end
|
|
283
293
|
update(ends_at: nil, status: @api_record.status)
|
|
294
|
+
|
|
295
|
+
# Stripe resumes successfully even when the payment fails, leaving the subscription paused
|
|
296
|
+
validate_latest_payment if status_paused?
|
|
297
|
+
true
|
|
284
298
|
rescue ::Stripe::StripeError => e
|
|
285
299
|
raise Pay::Stripe::Error, e
|
|
286
300
|
end
|
|
@@ -303,9 +317,7 @@ module Pay
|
|
|
303
317
|
)
|
|
304
318
|
|
|
305
319
|
# Validate that swap was successful and handle SCA if needed
|
|
306
|
-
|
|
307
|
-
Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
|
|
308
|
-
end
|
|
320
|
+
validate_latest_payment
|
|
309
321
|
|
|
310
322
|
sync!(object: @api_record)
|
|
311
323
|
rescue ::Stripe::StripeError => e
|
|
@@ -369,6 +381,23 @@ module Pay
|
|
|
369
381
|
|
|
370
382
|
private
|
|
371
383
|
|
|
384
|
+
# Stripe sets the status to `paused` when a trial ends without a payment method
|
|
385
|
+
def status_paused?
|
|
386
|
+
status == "paused"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Pausing collection with `void` leaves the status unchanged
|
|
390
|
+
def pause_behavior_void?
|
|
391
|
+
pause_behavior == "void"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if the latest invoice's payment is incomplete
|
|
395
|
+
def validate_latest_payment
|
|
396
|
+
if (payment_intent = latest_payment)
|
|
397
|
+
Pay::Payment.new(payment_intent, stripe_account: stripe_account).validate
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
372
401
|
# Options for Stripe requests
|
|
373
402
|
def stripe_options
|
|
374
403
|
{stripe_account: stripe_account}.compact
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
<% scope = @payment.setup_intent? ? "pay.setup_intent" : "pay" %>
|
|
1
2
|
<div
|
|
2
3
|
data-controller="payment-intent"
|
|
3
4
|
data-payment-intent-public-key-value="<%= Pay::Stripe.public_key %>"
|
|
4
5
|
data-payment-intent-stripe-account-value="<%= @payment.stripe_account %>"
|
|
5
6
|
data-payment-intent-client-secret-value="<%= @payment.client_secret %>"
|
|
6
7
|
data-payment-intent-status-value="<%= @payment.status %>"
|
|
8
|
+
data-payment-intent-setup-intent-value="<%= @payment.setup_intent? %>"
|
|
7
9
|
data-payment-intent-customer-value="<%= @payment.customer %>"
|
|
8
10
|
data-payment-intent-name-missing-message-value="<%= t("pay.requires_action.name_missing") %>"
|
|
9
|
-
data-payment-intent-confirmed-message-value="<%= t("
|
|
11
|
+
data-payment-intent-confirmed-message-value="<%= t("requires_action.success", scope: scope) %>"
|
|
10
12
|
class="h-screen md:flex md:justify-center md:items-center">
|
|
11
13
|
<div class="w-full max-w-lg">
|
|
12
14
|
<!-- Status Messages -->
|
|
@@ -28,18 +30,18 @@
|
|
|
28
30
|
|
|
29
31
|
<div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
|
|
30
32
|
<% if @payment.succeeded? %>
|
|
31
|
-
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "
|
|
32
|
-
<p class="mb-6"><%=t "
|
|
33
|
+
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "successful.header", scope: scope %></h1>
|
|
34
|
+
<p class="mb-6"><%=t "successful.description", scope: scope %></p>
|
|
33
35
|
|
|
34
36
|
<% elsif @payment.canceled? %>
|
|
35
|
-
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "
|
|
36
|
-
<p class="mb-6"><%=t "
|
|
37
|
+
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "cancelled.header", scope: scope %></h1>
|
|
38
|
+
<p class="mb-6"><%=t "cancelled.description", scope: scope %></p>
|
|
37
39
|
|
|
38
40
|
<% else %>
|
|
39
41
|
<div data-payment-intent-target="form" id="payment-elements">
|
|
40
42
|
<!-- Instructions -->
|
|
41
|
-
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "
|
|
42
|
-
<p class="mb-6"><%=t "
|
|
43
|
+
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "requires_action.header", scope: scope, amount: @payment.amount_with_currency %></h1>
|
|
44
|
+
<p class="mb-6"><%=t "requires_action.description", scope: scope %></p>
|
|
43
45
|
|
|
44
46
|
<div data-payment-intent-target="cardFields" class="hidden">
|
|
45
47
|
<!-- Name -->
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
|
|
54
56
|
<!-- Pay Button -->
|
|
55
57
|
<button data-payment-intent-target="button" data-action="click->payment-intent#confirmPayment" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500">
|
|
56
|
-
<%=t "
|
|
58
|
+
<%=t "requires_action.button", scope: scope, amount: @payment.amount_with_currency %>
|
|
57
59
|
</button>
|
|
58
60
|
</div>
|
|
59
61
|
<% end %>
|
|
@@ -79,6 +81,7 @@
|
|
|
79
81
|
clientSecret: String,
|
|
80
82
|
customer: String,
|
|
81
83
|
status: String,
|
|
84
|
+
setupIntent: Boolean,
|
|
82
85
|
nameMissingMessage: String,
|
|
83
86
|
confirmedMessage: String,
|
|
84
87
|
errorMessage: String,
|
|
@@ -104,7 +107,7 @@
|
|
|
104
107
|
switch(this.statusValue) {
|
|
105
108
|
case "requires_action":
|
|
106
109
|
this.processingValue = true
|
|
107
|
-
this.
|
|
110
|
+
this.confirm({clientSecret: this.clientSecretValue})
|
|
108
111
|
break
|
|
109
112
|
case "requires_payment_method":
|
|
110
113
|
this.cardFieldsTarget.classList.toggle("hidden", false)
|
|
@@ -114,7 +117,14 @@
|
|
|
114
117
|
|
|
115
118
|
confirmPayment() {
|
|
116
119
|
this.processingValue = true
|
|
117
|
-
this.
|
|
120
|
+
this.confirm({elements: this.elements})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SetupIntents (saving a payment method) and PaymentIntents confirm through different Stripe.js methods
|
|
124
|
+
confirm(options) {
|
|
125
|
+
const params = {...options, confirmParams: { return_url: document.location.href }}
|
|
126
|
+
const result = this.setupIntentValue ? this.stripe.confirmSetup(params) : this.stripe.confirmPayment(params)
|
|
127
|
+
result.then(this.handleConfirmResult.bind(this))
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
handleConfirmResult(result) {
|
|
@@ -126,7 +136,8 @@
|
|
|
126
136
|
this.errorMessageValue = this.nameMissingMessageValue
|
|
127
137
|
} else {
|
|
128
138
|
this.errorMessageValue = result.error.message
|
|
129
|
-
|
|
139
|
+
const intent = result.error.payment_intent || result.error.setup_intent
|
|
140
|
+
if (intent) { this.statusValue = intent.status }
|
|
130
141
|
}
|
|
131
142
|
} else {
|
|
132
143
|
this.completeValue = true
|
data/config/locales/en.yml
CHANGED
|
@@ -14,6 +14,18 @@ en:
|
|
|
14
14
|
button: "Pay %{amount}"
|
|
15
15
|
name_missing: "Please provide your name."
|
|
16
16
|
success: "The payment was successful."
|
|
17
|
+
setup_intent:
|
|
18
|
+
successful:
|
|
19
|
+
header: "Payment Method Saved"
|
|
20
|
+
description: "Your payment method was successfully confirmed."
|
|
21
|
+
cancelled:
|
|
22
|
+
header: "Setup Cancelled"
|
|
23
|
+
description: "This payment method setup was cancelled."
|
|
24
|
+
requires_action:
|
|
25
|
+
header: "Confirm your payment method"
|
|
26
|
+
description: "Extra confirmation is needed to save your payment method. Please confirm it by filling out your payment details below."
|
|
27
|
+
button: "Save payment method"
|
|
28
|
+
success: "Your payment method was saved."
|
|
17
29
|
all_rights_reserved: "All rights reserved."
|
|
18
30
|
back: "Go back"
|
|
19
31
|
refund: "Refund"
|
data/lib/pay/payment.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Pay
|
|
|
19
19
|
@stripe_account = stripe_account
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def setup_intent?
|
|
23
|
+
intent.is_a?(::Stripe::SetupIntent)
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def requires_payment_method?
|
|
23
27
|
status == "requires_payment_method"
|
|
24
28
|
end
|
|
@@ -35,7 +39,9 @@ module Pay
|
|
|
35
39
|
status == "succeeded"
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
# SetupIntents save a payment method for later and have no amount
|
|
38
43
|
def amount_with_currency
|
|
44
|
+
return if setup_intent?
|
|
39
45
|
Pay::Currency.format(amount, currency: currency)
|
|
40
46
|
end
|
|
41
47
|
|
data/lib/pay/version.rb
CHANGED