purchasekit 0.4.5 → 0.6.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: b1a96aa9528fbd3b75db3c4b8846925eec2ad386d3880e8eee945419394e9f33
4
- data.tar.gz: 63d645c076363e6a8f8798925d5e9e988e0870938302a5c171534a3f9d4b6935
3
+ metadata.gz: '06259cd19ab57561d46b97026a0330fbd7d47792d84971319e2893b3adc5b8a6'
4
+ data.tar.gz: be841b7642f9663472c5ec047bf144b065485e91c637864afc3430016340990e
5
5
  SHA512:
6
- metadata.gz: 87b85b9254ee1c59e2d65eb9f57e8f9aa851b7eea87f9576252e4679f85667036b979de0bc8ddefabe415de27125f846e71e64156ea7dc5b3a6404b006936862
7
- data.tar.gz: 2a55629192a0b7aa000d3c43f6dec35c34ef0854338c3b7f6814877cbbd93dd291c504fde26caab396245bc6142c51259ec8832b03c1f735e2ff30fe78e3058a
6
+ metadata.gz: 317a861f889c92f94479fc1d540aa671466105b0a494566d36f8b9ec9a59df9f2b95d735eba02d456115cc7157b7a7180f420ab09df8da179a6bdd5747304931
7
+ data.tar.gz: d8f8de0e095619fe87839deb6cedd9b51cf9eabbd3707fcd74cb2de2df41ceea42d09c9997535a827cd66062e597b5a54f2bb670b5810ef6b5231e49e47f9b47
data/README.md CHANGED
@@ -140,12 +140,23 @@ Build a paywall using the included helper. Subscribe to the Turbo Stream for rea
140
140
  <% end %>
141
141
 
142
142
  <%= paywall.submit "Subscribe" %>
143
+ <%= paywall.restore url: restore_purchases_path, class: "btn btn-link" %>
143
144
  <% end %>
145
+ ```
146
+
147
+ ### Restore purchases
148
+
149
+ Apple requires apps with in-app purchases to include a "Restore purchases" button. This handles users who switch devices or reinstall the app.
144
150
 
145
- <%= button_to "Restore purchases", restore_purchases_path %>
151
+ The `paywall.restore` helper renders a button that reads active subscriptions directly from StoreKit (iOS) or Play Billing (Android) via the native bridge. Pass a `url:` to automatically POST the subscription IDs to your server:
152
+
153
+ ```erb
154
+ <%= paywall.restore url: restore_purchases_path, class: "btn btn-link" %>
146
155
  ```
147
156
 
148
- The restore link checks your server for an active subscription. Implement the endpoint in your app:
157
+ When the user taps restore, the JS controller sends a bridge message to the native app, receives the active subscription IDs, and POSTs them as JSON to your URL. If the server responds with a redirect, the page navigates automatically.
158
+
159
+ On the server, match the IDs against your stored subscriptions. The `subscription_ids` match the `subscription_id` field in PurchaseKit webhook payloads (Apple's `originalTransactionId`, Google's order ID):
149
160
 
150
161
  ```ruby
151
162
  # routes.rb
@@ -153,7 +164,9 @@ post "restore_purchases", to: "subscriptions#restore"
153
164
 
154
165
  # subscriptions_controller.rb
155
166
  def restore
156
- if current_user.subscribed?
167
+ ids = params[:subscription_ids] || []
168
+
169
+ if ids.any? && current_user.subscriptions.where(processor_id: ids).active.any?
157
170
  redirect_to dashboard_path, notice: "Your subscription is active."
158
171
  else
159
172
  redirect_to paywall_path, alert: "No active subscription found."
@@ -161,6 +174,15 @@ def restore
161
174
  end
162
175
  ```
163
176
 
177
+ If you need custom behavior, omit the `url:` and listen for the DOM event instead:
178
+
179
+ ```javascript
180
+ document.addEventListener("purchasekit--paywall:restore", (event) => {
181
+ const { subscriptionIds, error } = event.detail
182
+ // Handle as needed
183
+ })
184
+ ```
185
+
164
186
  Products are fetched from the PurchaseKit API:
165
187
 
166
188
  ```ruby
@@ -52,7 +52,8 @@ module PurchaseKit
52
52
  data: {
53
53
  purchasekit__paywall_target: "planRadio",
54
54
  apple_store_product_id: product.apple_product_id,
55
- google_store_product_id: product.google_product_id
55
+ google_store_product_id: product.google_product_id,
56
+ google_store_base_plan_id: product.google_base_plan_id
56
57
  }
57
58
  )
58
59
 
@@ -69,7 +70,8 @@ module PurchaseKit
69
70
  data = (options.delete(:data) || {}).merge(
70
71
  purchasekit__paywall_target: "price",
71
72
  apple_store_product_id: @current_product.apple_product_id,
72
- google_store_product_id: @current_product.google_product_id
73
+ google_store_product_id: @current_product.google_product_id,
74
+ google_store_base_plan_id: @current_product.google_base_plan_id
73
75
  )
74
76
 
75
77
  loading_content = block ? @template.capture(&block) : "Loading..."
@@ -85,5 +87,15 @@ module PurchaseKit
85
87
  @template.submit_tag(text, disabled: true, data: data, **options)
86
88
  end
87
89
 
90
+ def restore(text = "Restore purchases", url: nil, **options)
91
+ data = (options.delete(:data) || {}).merge(
92
+ purchasekit__paywall_target: "restoreButton",
93
+ action: "purchasekit--paywall#restore"
94
+ )
95
+ data[:restore_url] = url if url
96
+
97
+ @template.content_tag(:button, text, type: "button", data: data, **options)
98
+ end
99
+
88
100
  end
89
101
  end
@@ -2,7 +2,7 @@ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
2
 
3
3
  export default class extends BridgeComponent {
4
4
  static component = "paywall"
5
- static targets = ["planRadio", "price", "submitButton", "response", "environment"]
5
+ static targets = ["planRadio", "price", "submitButton", "response", "environment", "restoreButton"]
6
6
 
7
7
  connect() {
8
8
  super.connect()
@@ -15,6 +15,26 @@ export default class extends BridgeComponent {
15
15
  }
16
16
  }
17
17
 
18
+ restore() {
19
+ if (this.hasRestoreButtonTarget) {
20
+ this.restoreButtonTarget.disabled = true
21
+ }
22
+
23
+ this.send("restore", {}, message => {
24
+ if (this.hasRestoreButtonTarget) {
25
+ this.restoreButtonTarget.disabled = false
26
+ }
27
+
28
+ const { subscriptionIds, error } = message.data
29
+ this.dispatch("restore", { detail: { subscriptionIds, error } })
30
+
31
+ const restoreUrl = this.hasRestoreButtonTarget && this.restoreButtonTarget.dataset.restoreUrl
32
+ if (!error && restoreUrl) {
33
+ this.#submitRestore(restoreUrl, subscriptionIds || [])
34
+ }
35
+ })
36
+ }
37
+
18
38
  responseTargetConnected(element) {
19
39
  const error = element.dataset.error
20
40
 
@@ -62,6 +82,26 @@ export default class extends BridgeComponent {
62
82
  })
63
83
  }
64
84
 
85
+ #submitRestore(url, subscriptionIds) {
86
+ const csrfToken = document.querySelector("meta[name=csrf-token]")?.content
87
+
88
+ fetch(url, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ ...(csrfToken && { "X-CSRF-Token": csrfToken })
93
+ },
94
+ body: JSON.stringify({ subscription_ids: subscriptionIds })
95
+ }).then(response => {
96
+ if (response.redirected) {
97
+ window.Turbo.visit(response.url)
98
+ }
99
+ }).catch(error => {
100
+ console.error("Restore request failed:", error)
101
+ alert("Something went wrong restoring purchases. Please try again.")
102
+ })
103
+ }
104
+
65
105
  #fallbackTimeoutId = null
66
106
 
67
107
  #fetchPrices() {
@@ -91,8 +131,8 @@ export default class extends BridgeComponent {
91
131
 
92
132
  #setPrices(prices) {
93
133
  this.priceTargets.forEach(el => {
94
- const { appleStoreProductId, googleStoreProductId } = this.#productIds(el)
95
- const price = prices[appleStoreProductId] || prices[googleStoreProductId]
134
+ const { appleStoreProductId, googleStoreProductId, googleStoreBasePlanId } = this.#productIds(el)
135
+ const price = prices[appleStoreProductId] || prices[googleStoreBasePlanId] || prices[googleStoreProductId]
96
136
 
97
137
  if (price) {
98
138
  el.textContent = price
@@ -105,7 +145,8 @@ export default class extends BridgeComponent {
105
145
  #productIds(element) {
106
146
  return {
107
147
  appleStoreProductId: element.dataset.appleStoreProductId,
108
- googleStoreProductId: element.dataset.googleStoreProductId
148
+ googleStoreProductId: element.dataset.googleStoreProductId,
149
+ googleStoreBasePlanId: element.dataset.googleStoreBasePlanId
109
150
  }
110
151
  }
111
152
 
@@ -2,6 +2,7 @@
2
2
  data-correlation-id="<%= intent.uuid %>"
3
3
  data-apple-store-product-id="<%= intent.product.apple_product_id %>"
4
4
  data-google-store-product-id="<%= intent.product.google_product_id %>"
5
+ data-google-store-base-plan-id="<%= intent.product.google_base_plan_id %>"
5
6
  data-xcode-completion-url="<%= intent.xcode_completion_url %>"
6
7
  data-success-path="<%= intent.success_path %>">
7
8
  </div>
@@ -14,7 +14,8 @@ module PurchaseKit
14
14
  Product.new(
15
15
  id: id,
16
16
  apple_product_id: product_data[:apple_product_id],
17
- google_product_id: product_data[:google_product_id]
17
+ google_product_id: product_data[:google_product_id],
18
+ google_base_plan_id: product_data[:google_base_plan_id]
18
19
  )
19
20
  end
20
21
  end
@@ -12,7 +12,8 @@ module PurchaseKit
12
12
  Product.new(
13
13
  id: response["id"],
14
14
  apple_product_id: response["apple_product_id"],
15
- google_product_id: response["google_product_id"]
15
+ google_product_id: response["google_product_id"],
16
+ google_base_plan_id: response["google_base_plan_id"]
16
17
  )
17
18
  when 404
18
19
  raise PurchaseKit::NotFoundError, "Product not found: #{id}"
@@ -11,12 +11,13 @@ module PurchaseKit
11
11
  # product.google_product_id # => "pro_annual"
12
12
  #
13
13
  class Product
14
- attr_reader :id, :apple_product_id, :google_product_id
14
+ attr_reader :id, :apple_product_id, :google_product_id, :google_base_plan_id
15
15
 
16
- def initialize(id:, apple_product_id: nil, google_product_id: nil)
16
+ def initialize(id:, apple_product_id: nil, google_product_id: nil, google_base_plan_id: nil)
17
17
  @id = id
18
18
  @apple_product_id = apple_product_id
19
19
  @google_product_id = google_product_id
20
+ @google_base_plan_id = google_base_plan_id
20
21
  end
21
22
 
22
23
  # Get the store-specific product ID for a platform.
@@ -19,7 +19,8 @@ module PurchaseKit
19
19
  product = Product.new(
20
20
  id: product_data["id"],
21
21
  apple_product_id: product_data["apple_product_id"],
22
- google_product_id: product_data["google_product_id"]
22
+ google_product_id: product_data["google_product_id"],
23
+ google_base_plan_id: product_data["google_base_plan_id"]
23
24
  )
24
25
  new(id: response["id"], uuid: response["uuid"], product: product, success_path: success_path)
25
26
  when 402
@@ -1,3 +1,3 @@
1
1
  module PurchaseKit
2
- VERSION = "0.4.5"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purchasekit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Masilotti
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-11 00:00:00.000000000 Z
11
+ date: 2026-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails