purchasekit 0.7.1 → 0.9.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/README.md +1 -0
- data/app/helpers/purchase_kit/paywall_helper.rb +8 -2
- data/app/javascript/controllers/purchasekit/paywall_controller.js +52 -8
- data/lib/purchasekit/events.rb +7 -0
- data/lib/purchasekit/pay/webhooks/subscription_created.rb +4 -1
- data/lib/purchasekit/pay/webhooks/subscription_updated.rb +4 -1
- data/lib/purchasekit/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4b7e933f67b6bb8b2d52dce69e7897d9e6834c58f593a44eb6c27629228d8fa
|
|
4
|
+
data.tar.gz: 2ff18e42f762b9613aedebbe3a05a12e0cb127cb425f4cce0d79c1010bcfdf3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eed594dc1d832cd950327956578f421598d68395c353d1aa3f1790d635ab744ca3fc8573d7888a5a1b34d8d19ba0308b929ed3eb215d2ca53fef001edb988520
|
|
7
|
+
data.tar.gz: 470631898b0a2b6872903299a2e4c947bac402c30eaea904eeb5eaa029c77d66f9f6878bfaf0fd485e842e6e62d2eeaaeec452e6f9e4dab46d6c4ec8be4982a8
|
data/README.md
CHANGED
|
@@ -113,6 +113,7 @@ end
|
|
|
113
113
|
| `event.subscription_id` | Store's subscription ID |
|
|
114
114
|
| `event.store` | `"apple"` or `"google"` |
|
|
115
115
|
| `event.store_product_id` | e.g., `"com.example.pro.annual"` |
|
|
116
|
+
| `event.google_base_plan_id` | Google Play base plan ID (e.g., `"annual"`) when using umbrella subscriptions, nil otherwise |
|
|
116
117
|
| `event.status` | `"active"`, `"canceled"`, `"expired"` |
|
|
117
118
|
| `event.current_period_start` | Start of billing period |
|
|
118
119
|
| `event.current_period_end` | End of billing period |
|
|
@@ -6,6 +6,11 @@ module PurchaseKit
|
|
|
6
6
|
# events. With Pay, this must be `Pay::Customer.id` (the webhook handler
|
|
7
7
|
# does `Pay::Customer.find(customer_id)`). Without Pay, use your own user ID.
|
|
8
8
|
# @param success_path [String] Where to redirect after successful purchase
|
|
9
|
+
# @param proration_mode [String] Google Play replacement mode used when a base
|
|
10
|
+
# plan is swapped within one umbrella subscription (e.g. monthly to annual).
|
|
11
|
+
# One of "charge_prorated_price" (default), "with_time_proration",
|
|
12
|
+
# "charge_full_price", "without_proration", or "deferred". Ignored on Apple,
|
|
13
|
+
# which handles intra-group upgrades and downgrades automatically.
|
|
9
14
|
# @yield [PaywallBuilder] Builder for plan options and buttons
|
|
10
15
|
#
|
|
11
16
|
# Example (with Pay):
|
|
@@ -17,14 +22,15 @@ module PurchaseKit
|
|
|
17
22
|
# <%= paywall.submit "Subscribe" %>
|
|
18
23
|
# <% end %>
|
|
19
24
|
#
|
|
20
|
-
def purchasekit_paywall(customer_id:, success_path: main_app.root_path, **options)
|
|
25
|
+
def purchasekit_paywall(customer_id:, success_path: main_app.root_path, proration_mode: "charge_prorated_price", **options)
|
|
21
26
|
raise ArgumentError, "customer_id is required" if customer_id.blank?
|
|
22
27
|
|
|
23
28
|
builder = PaywallBuilder.new(self)
|
|
24
29
|
|
|
25
30
|
form_data = (options.delete(:data) || {}).merge(
|
|
26
31
|
controller: "purchasekit--paywall",
|
|
27
|
-
purchasekit__paywall_customer_id_value: customer_id
|
|
32
|
+
purchasekit__paywall_customer_id_value: customer_id,
|
|
33
|
+
purchasekit__paywall_proration_mode_value: proration_mode
|
|
28
34
|
)
|
|
29
35
|
|
|
30
36
|
form_with(url: purchase_kit.purchases_path, id: "purchasekit_paywall", data: form_data, **options) do |form|
|
|
@@ -3,6 +3,7 @@ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
|
|
|
3
3
|
export default class extends BridgeComponent {
|
|
4
4
|
static component = "paywall"
|
|
5
5
|
static targets = ["planRadio", "price", "submitButton", "response", "environment", "restoreButton"]
|
|
6
|
+
static values = { prorationMode: { type: String, default: "charge_prorated_price" } }
|
|
6
7
|
|
|
7
8
|
connect() {
|
|
8
9
|
super.connect()
|
|
@@ -13,6 +14,7 @@ export default class extends BridgeComponent {
|
|
|
13
14
|
if (this.#fallbackTimeoutId) {
|
|
14
15
|
clearTimeout(this.#fallbackTimeoutId)
|
|
15
16
|
}
|
|
17
|
+
this.#stopWatchingForCompletion()
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
restore() {
|
|
@@ -53,11 +55,14 @@ export default class extends BridgeComponent {
|
|
|
53
55
|
|
|
54
56
|
element.remove()
|
|
55
57
|
this.#disableForm()
|
|
58
|
+
this.dispatch("initiated", { detail: { correlationId } })
|
|
56
59
|
this.#triggerNativePurchase(productIds, correlationId, xcodeCompletionUrl, successPath)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
#triggerNativePurchase(productIds, correlationId, xcodeCompletionUrl, successPath) {
|
|
60
|
-
|
|
63
|
+
// googleStoreProrationMode only applies on Android plan swaps; iOS ignores it.
|
|
64
|
+
const googleStoreProrationMode = this.prorationModeValue
|
|
65
|
+
this.send("purchase", { ...productIds, correlationId, xcodeCompletionUrl, googleStoreProrationMode }, message => {
|
|
61
66
|
const { status, error } = message.data
|
|
62
67
|
|
|
63
68
|
if (error) {
|
|
@@ -72,16 +77,53 @@ export default class extends BridgeComponent {
|
|
|
72
77
|
return
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
window.Turbo.visit(successPath)
|
|
80
|
-
}, 30000)
|
|
81
|
-
}
|
|
80
|
+
// The store confirmed the purchase. The Pay::Subscription (and the redirect)
|
|
81
|
+
// still depend on the server webhook, so move into the awaiting state.
|
|
82
|
+
this.dispatch("store-confirmed", { detail: { status } })
|
|
83
|
+
this.#awaitCompletion(successPath)
|
|
82
84
|
})
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// Waits for the webhook-driven redirect after the store confirms a purchase.
|
|
88
|
+
// The redirect arrives as a Turbo Stream "redirect" action over ActionCable, with
|
|
89
|
+
// a 30-second fallback for when ActionCable isn't connected.
|
|
90
|
+
#awaitCompletion(successPath) {
|
|
91
|
+
this.dispatch("awaiting-webhook", { detail: {} })
|
|
92
|
+
|
|
93
|
+
this.#streamRenderListener = event => {
|
|
94
|
+
if (event.target?.getAttribute("action") === "redirect") {
|
|
95
|
+
this.#complete()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
document.addEventListener("turbo:before-stream-render", this.#streamRenderListener)
|
|
99
|
+
|
|
100
|
+
if (successPath) {
|
|
101
|
+
this.#fallbackTimeoutId = setTimeout(() => {
|
|
102
|
+
this.#complete()
|
|
103
|
+
window.Turbo.visit(successPath)
|
|
104
|
+
}, 30000)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#complete() {
|
|
109
|
+
if (this.#completed) return
|
|
110
|
+
this.#completed = true
|
|
111
|
+
|
|
112
|
+
if (this.#fallbackTimeoutId) {
|
|
113
|
+
clearTimeout(this.#fallbackTimeoutId)
|
|
114
|
+
this.#fallbackTimeoutId = null
|
|
115
|
+
}
|
|
116
|
+
this.#stopWatchingForCompletion()
|
|
117
|
+
this.dispatch("complete", { detail: {} })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#stopWatchingForCompletion() {
|
|
121
|
+
if (this.#streamRenderListener) {
|
|
122
|
+
document.removeEventListener("turbo:before-stream-render", this.#streamRenderListener)
|
|
123
|
+
this.#streamRenderListener = null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
#submitRestore(url, subscriptionIds) {
|
|
86
128
|
const csrfToken = document.querySelector("meta[name=csrf-token]")?.content
|
|
87
129
|
|
|
@@ -103,6 +145,8 @@ export default class extends BridgeComponent {
|
|
|
103
145
|
}
|
|
104
146
|
|
|
105
147
|
#fallbackTimeoutId = null
|
|
148
|
+
#streamRenderListener = null
|
|
149
|
+
#completed = false
|
|
106
150
|
|
|
107
151
|
#fetchPrices() {
|
|
108
152
|
const products = this.priceTargets.map(el => this.#productIds(el))
|
data/lib/purchasekit/events.rb
CHANGED
|
@@ -94,6 +94,13 @@ module PurchaseKit
|
|
|
94
94
|
payload[:store_product_id]
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# The Google Play base plan ID (e.g., "annual", "monthly").
|
|
98
|
+
# Only present when the purchase used Google's umbrella subscription
|
|
99
|
+
# with multiple base plans. Nil for Apple and for flat Google products.
|
|
100
|
+
def google_base_plan_id
|
|
101
|
+
payload[:google_base_plan_id]
|
|
102
|
+
end
|
|
103
|
+
|
|
97
104
|
# The subscription name you configured in PurchaseKit
|
|
98
105
|
def subscription_name
|
|
99
106
|
payload[:subscription_name]
|
|
@@ -23,7 +23,10 @@ module PurchaseKit
|
|
|
23
23
|
current_period_end: parse_time(event["current_period_end"]),
|
|
24
24
|
trial_ends_at: parse_time(event["trial_ends_at"]),
|
|
25
25
|
ends_at: parse_time(event["ends_at"]),
|
|
26
|
-
data: (subscription.data || {}).merge(
|
|
26
|
+
data: (subscription.data || {}).merge(
|
|
27
|
+
"store" => event["store"],
|
|
28
|
+
"google_base_plan_id" => event["google_base_plan_id"]
|
|
29
|
+
).compact
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
return unless is_new
|
|
@@ -22,7 +22,10 @@ module PurchaseKit
|
|
|
22
22
|
current_period_end: parse_time(event["current_period_end"]),
|
|
23
23
|
trial_ends_at: parse_time(event["trial_ends_at"]),
|
|
24
24
|
ends_at: parse_time(event["ends_at"]),
|
|
25
|
-
data: (subscription.data || {}).merge(
|
|
25
|
+
data: (subscription.data || {}).merge(
|
|
26
|
+
"store" => event["store"],
|
|
27
|
+
"google_base_plan_id" => event["google_base_plan_id"]
|
|
28
|
+
).compact
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
broadcast_redirect(customer, event) if event["success_path"].present?
|
data/lib/purchasekit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: purchasekit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joe Masilotti
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -224,6 +225,7 @@ metadata:
|
|
|
224
225
|
homepage_uri: https://purchasekit.com
|
|
225
226
|
source_code_uri: https://github.com/purchasekit/purchasekit
|
|
226
227
|
changelog_uri: https://github.com/purchasekit/purchasekit/blob/main/CHANGELOG.md
|
|
228
|
+
post_install_message:
|
|
227
229
|
rdoc_options: []
|
|
228
230
|
require_paths:
|
|
229
231
|
- lib
|
|
@@ -238,7 +240,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
238
240
|
- !ruby/object:Gem::Version
|
|
239
241
|
version: '0'
|
|
240
242
|
requirements: []
|
|
241
|
-
rubygems_version:
|
|
243
|
+
rubygems_version: 3.2.3
|
|
244
|
+
signing_key:
|
|
242
245
|
specification_version: 4
|
|
243
246
|
summary: In-app purchase infrastructure for Rails
|
|
244
247
|
test_files: []
|