purchasekit 0.4.5 → 0.5.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: 262fd99889485c18c0d9d668d930fc0cec3b258620396ef960b8d8828a040e29
4
+ data.tar.gz: 2b07a7abf7c580e72f98896e8e0a87a2cd7008cccbddbac8ba5d481e12c7308d
5
5
  SHA512:
6
- metadata.gz: 87b85b9254ee1c59e2d65eb9f57e8f9aa851b7eea87f9576252e4679f85667036b979de0bc8ddefabe415de27125f846e71e64156ea7dc5b3a6404b006936862
7
- data.tar.gz: 2a55629192a0b7aa000d3c43f6dec35c34ef0854338c3b7f6814877cbbd93dd291c504fde26caab396245bc6142c51259ec8832b03c1f735e2ff30fe78e3058a
6
+ metadata.gz: 66ac5a6194010cfaa573d54eaa11df03a6000778e3d828b35476d12d6ba4474ce233018043adcd166187ab309db3dd009d65b9759d36019c6f48391b5672fd21
7
+ data.tar.gz: a2385ee1ef72d178fe0354f0631a6cc4d258ebcedb2f3ae1a779974b6b9698079bd68854286bf30288ef1cb4284187abfc9000228a18569678a4466a706354e1
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
@@ -85,5 +85,15 @@ module PurchaseKit
85
85
  @template.submit_tag(text, disabled: true, data: data, **options)
86
86
  end
87
87
 
88
+ def restore(text = "Restore purchases", url: nil, **options)
89
+ data = (options.delete(:data) || {}).merge(
90
+ purchasekit__paywall_target: "restoreButton",
91
+ action: "purchasekit--paywall#restore"
92
+ )
93
+ data[:restore_url] = url if url
94
+
95
+ @template.content_tag(:button, text, type: "button", data: data, **options)
96
+ end
97
+
88
98
  end
89
99
  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() {
@@ -1,3 +1,3 @@
1
1
  module PurchaseKit
2
- VERSION = "0.4.5"
2
+ VERSION = "0.5.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.5.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-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails