purchasekit 0.8.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d05f29f2c2248e33c8ee228dc191914f6aa66174505f5ddd3612987258c23a8
4
- data.tar.gz: 1734e423d697dacab4cdfcf67bee287d43c48b6c5c40d38f229a1e2536ba5032
3
+ metadata.gz: c4b7e933f67b6bb8b2d52dce69e7897d9e6834c58f593a44eb6c27629228d8fa
4
+ data.tar.gz: 2ff18e42f762b9613aedebbe3a05a12e0cb127cb425f4cce0d79c1010bcfdf3e
5
5
  SHA512:
6
- metadata.gz: 6d1226d312912c774fef81504560f1bdbb8ec9be76d260588647494243e6319fa240a04f72e98048d6cd8ccf30f06754be95517c5ab79b2f174dbb839a7aaace
7
- data.tar.gz: 88aa46c038eb5d35603df2a010fc4443949c8d25fbcdb7f17ecbbaa5e736422d8e8ea82652b0d8950d67245751f3717cd3ec5ba0fd6802756ef00e7b8a6e31e1
6
+ metadata.gz: eed594dc1d832cd950327956578f421598d68395c353d1aa3f1790d635ab744ca3fc8573d7888a5a1b34d8d19ba0308b929ed3eb215d2ca53fef001edb988520
7
+ data.tar.gz: 470631898b0a2b6872903299a2e4c947bac402c30eaea904eeb5eaa029c77d66f9f6878bfaf0fd485e842e6e62d2eeaaeec452e6f9e4dab46d6c4ec8be4982a8
@@ -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
- this.send("purchase", { ...productIds, correlationId, xcodeCompletionUrl }, message => {
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
- // On success, Turbo Stream will broadcast redirect when webhook completes.
76
- // Fallback: redirect after 30 seconds in case ActionCable isn't connected.
77
- if (successPath) {
78
- this.#fallbackTimeoutId = setTimeout(() => {
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))
@@ -1,3 +1,3 @@
1
1
  module PurchaseKit
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
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.8.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: 1980-01-02 00:00:00.000000000 Z
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: 4.0.3
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: []