purchasekit-pay 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +159 -0
- data/Rakefile +10 -0
- data/app/controllers/purchase_kit/pay/application_controller.rb +2 -0
- data/app/controllers/purchase_kit/pay/purchases_controller.rb +32 -0
- data/app/controllers/purchase_kit/pay/webhooks_controller.rb +32 -0
- data/app/helpers/purchase_kit/pay/paywall_helper.rb +85 -0
- data/app/javascript/controllers/purchasekit_pay/paywall_controller.js +111 -0
- data/app/javascript/purchasekit_pay/turbo_actions.js +4 -0
- data/app/models/pay/purchasekit/charge.rb +9 -0
- data/app/models/pay/purchasekit/customer.rb +22 -0
- data/app/models/pay/purchasekit/subscription.rb +33 -0
- data/app/views/purchase_kit/pay/purchases/_subscription_required.html.erb +3 -0
- data/app/views/purchase_kit/pay/purchases/create.turbo_stream.erb +7 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +4 -0
- data/lib/pay/purchasekit.rb +25 -0
- data/lib/purchasekit/api_client.rb +38 -0
- data/lib/purchasekit/pay/configuration.rb +23 -0
- data/lib/purchasekit/pay/engine.rb +35 -0
- data/lib/purchasekit/pay/error.rb +12 -0
- data/lib/purchasekit/pay/version.rb +5 -0
- data/lib/purchasekit/pay/webhook.rb +36 -0
- data/lib/purchasekit/pay/webhooks/base.rb +25 -0
- data/lib/purchasekit/pay/webhooks/subscription_canceled.rb +11 -0
- data/lib/purchasekit/pay/webhooks/subscription_created.rb +39 -0
- data/lib/purchasekit/pay/webhooks/subscription_expired.rb +11 -0
- data/lib/purchasekit/pay/webhooks/subscription_updated.rb +32 -0
- data/lib/purchasekit/pay/webhooks.rb +11 -0
- data/lib/purchasekit/product.rb +38 -0
- data/lib/purchasekit/purchase/intent.rb +41 -0
- data/lib/purchasekit-pay.rb +11 -0
- metadata +153 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e5043601031b8440098cdc344c6a9986d3add5bce85f1c5523dfa9506c05a363
|
|
4
|
+
data.tar.gz: e3570d5b0d442e19144b797c964923818d02f477596f0d6eb542a458cb99842a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f7a68f3b375bfd13268907f4c3002d1b728b75fbbfc08221aea8e0ee2c753c334fc56b786b0d89e68e6c8d08467fb0e8e4869855547601fe54e83662027f6e4e
|
|
7
|
+
data.tar.gz: e09eafd1dd15c6d228cfa57d460fd4f78f98dae7b02da02cac200bfe36610d0fe0e311fba6f430cb22117e793c57e419552986d182580c1be02847fa313f32a2
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2025 Joe Masilotti
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# PurchaseKit::Pay
|
|
2
|
+
|
|
3
|
+
PurchaseKit payment processor for the [Pay gem](https://github.com/pay-rails/pay).
|
|
4
|
+
|
|
5
|
+
Add mobile in-app purchases (IAP) to your Rails app with [PurchaseKit](https://purchasekit.dev) and Pay.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "purchasekit-pay"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
Configure your PurchaseKit API key:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# config/initializers/purchasekit.rb
|
|
27
|
+
PurchaseKit::Pay.configure do |config|
|
|
28
|
+
config.api_key = Rails.application.credentials.dig(:purchasekit, :api_key)
|
|
29
|
+
config.app_id = Rails.application.credentials.dig(:purchasekit, :app_id)
|
|
30
|
+
config.webhook_secret = Rails.application.credentials.dig(:purchasekit, :webhook_secret)
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Mount the engine in your routes:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# config/routes.rb
|
|
38
|
+
mount PurchaseKit::Pay::Engine, at: "/purchasekit"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### JavaScript setup
|
|
42
|
+
|
|
43
|
+
Import the Turbo Stream actions in your application:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
// app/javascript/application.js
|
|
47
|
+
import "purchasekit-pay/turbo_actions"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Register the gem's Stimulus controllers:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// app/javascript/controllers/index.js
|
|
54
|
+
import { application } from "controllers/application"
|
|
55
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
56
|
+
|
|
57
|
+
eagerLoadControllersFrom("controllers", application)
|
|
58
|
+
eagerLoadControllersFrom("purchasekit-pay", application)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Adding a paywall
|
|
64
|
+
|
|
65
|
+
First, ensure your user has a PurchaseKit payment processor:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
current_user.set_payment_processor(:purchasekit)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Fetch products in your controller:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
@annual = PurchaseKit::Product.find("prod_XXX")
|
|
75
|
+
@monthly = PurchaseKit::Product.find("prod_YYY")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then render a paywall using the builder pattern:
|
|
79
|
+
|
|
80
|
+
```erb
|
|
81
|
+
<%# Subscribe to ActionCable for real-time redirect after purchase %>
|
|
82
|
+
<%= turbo_stream_from dom_id(current_user.payment_processor) %>
|
|
83
|
+
|
|
84
|
+
<%= purchasekit_paywall customer: current_user.payment_processor, success_path: dashboard_path do |paywall| %>
|
|
85
|
+
<%= paywall.plan_option product: @annual, selected: true do %>
|
|
86
|
+
<span>Annual</span>
|
|
87
|
+
<%= paywall.price %>
|
|
88
|
+
<% end %>
|
|
89
|
+
|
|
90
|
+
<%= paywall.plan_option product: @monthly do %>
|
|
91
|
+
<span>Monthl</span>
|
|
92
|
+
<%= paywall.price %>
|
|
93
|
+
<% end %>
|
|
94
|
+
|
|
95
|
+
<%= paywall.submit "Subscribe", class: "btn btn-primary" %>
|
|
96
|
+
<%= paywall.restore_link %>
|
|
97
|
+
<% end %>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Paywall helper options
|
|
101
|
+
|
|
102
|
+
- `customer:` (required) - A `Pay::Customer` instance
|
|
103
|
+
- `success_path:` - Where to redirect after successful purchase (defaults to `root_path`)
|
|
104
|
+
|
|
105
|
+
### Builder methods
|
|
106
|
+
|
|
107
|
+
- `plan_option(product:, selected: false)` - Radio button and label for a plan
|
|
108
|
+
- `price` - Displays the localized price (must be inside `plan_option` block)
|
|
109
|
+
- `submit(text)` - Submit button (disabled until prices load)
|
|
110
|
+
- `restore_link(text: "Restore purchases")` - Link to restore previous purchases
|
|
111
|
+
|
|
112
|
+
### How it works
|
|
113
|
+
|
|
114
|
+
1. Page loads, Stimulus controller requests prices from native app via Hotwire Native Bridge
|
|
115
|
+
2. User selects a plan and taps subscribe
|
|
116
|
+
3. Form submits to PurchasesController, which creates a purchase intent with PurchaseKit
|
|
117
|
+
4. Native app handles the App Store/Play Store purchase flow
|
|
118
|
+
5. PurchaseKit receives webhook from Apple/Google, normalizes it, and POSTs to your app
|
|
119
|
+
6. Webhook handler creates `Pay::Subscription` and broadcasts a Turbo Stream redirect
|
|
120
|
+
7. User is redirected to `success_path`
|
|
121
|
+
|
|
122
|
+
## Webhook events
|
|
123
|
+
|
|
124
|
+
The gem handles these webhook events from the PurchaseKit:
|
|
125
|
+
|
|
126
|
+
- `subscription.created` - Creates a new `Pay::Subscription`
|
|
127
|
+
- `subscription.updated` - Updates subscription status and period
|
|
128
|
+
- `subscription.canceled` - Marks subscription as canceled
|
|
129
|
+
- `subscription.expired` - Marks subscription as expired
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- Ruby 3.1+
|
|
134
|
+
- Rails 7.0 - 8.x
|
|
135
|
+
- Pay 11.4+
|
|
136
|
+
- Turbo Rails (for ActionCable broadcasts)
|
|
137
|
+
- Stimulus
|
|
138
|
+
- Hotwire Native app with PurchaseKit bridge component
|
|
139
|
+
|
|
140
|
+
### JavaScript dependencies
|
|
141
|
+
|
|
142
|
+
This gem vendors and pins:
|
|
143
|
+
|
|
144
|
+
- **@hotwired/hotwire-native-bridge** (v1.2.2)
|
|
145
|
+
|
|
146
|
+
If your app already pins this package, your version takes precedence.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
After cloning the repo, run:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
bundle install
|
|
154
|
+
bundle exec rake test
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class PurchaseKit::Pay::PurchasesController < PurchaseKit::Pay::ApplicationController
|
|
2
|
+
rescue_from PurchaseKit::Pay::SubscriptionRequiredError, with: :subscription_required
|
|
3
|
+
|
|
4
|
+
def create
|
|
5
|
+
@customer = ::Pay::Customer.find(params[:customer_id])
|
|
6
|
+
|
|
7
|
+
@intent = PurchaseKit::Purchase::Intent.create(
|
|
8
|
+
product_id: params[:product_id],
|
|
9
|
+
customer_id: @customer.id,
|
|
10
|
+
success_path: params[:success_path],
|
|
11
|
+
environment: params[:environment]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.turbo_stream
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def subscription_required(exception)
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.turbo_stream do
|
|
24
|
+
render turbo_stream: turbo_stream.replace(
|
|
25
|
+
"purchasekit_paywall",
|
|
26
|
+
partial: "purchase_kit/pay/purchases/subscription_required",
|
|
27
|
+
locals: {message: exception.message}
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class PurchaseKit::Pay::WebhooksController < PurchaseKit::Pay::ApplicationController
|
|
2
|
+
skip_forgery_protection
|
|
3
|
+
|
|
4
|
+
def create
|
|
5
|
+
PurchaseKit::Pay::Webhook.queue(verified_event)
|
|
6
|
+
head :ok
|
|
7
|
+
rescue SignatureVerificationError => e
|
|
8
|
+
Rails.logger.error "[PurchaseKit] Webhook signature error: #{e.message}"
|
|
9
|
+
head :bad_request
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def verified_event
|
|
15
|
+
payload = request.raw_post
|
|
16
|
+
signature = request.headers["X-PurchaseKit-Signature"]
|
|
17
|
+
secret = PurchaseKit::Pay.config.webhook_secret
|
|
18
|
+
|
|
19
|
+
return JSON.parse(payload, symbolize_names: true) if secret.blank?
|
|
20
|
+
|
|
21
|
+
raise SignatureVerificationError, "Missing signature" if signature.blank?
|
|
22
|
+
|
|
23
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
|
|
24
|
+
unless ActiveSupport::SecurityUtils.secure_compare(signature, expected)
|
|
25
|
+
raise SignatureVerificationError, "Invalid signature"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
JSON.parse(payload, symbolize_names: true)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class SignatureVerificationError < StandardError; end
|
|
32
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module PurchaseKit::Pay::PaywallHelper
|
|
2
|
+
# Wrapper for paywall with bridge controller
|
|
3
|
+
# Renders a form that posts to the purchases endpoint
|
|
4
|
+
# Yields a builder for plan options and buttons
|
|
5
|
+
# Requires a Pay::Customer - call user.set_payment_processor(:purchasekit) first
|
|
6
|
+
def purchasekit_paywall(customer:, success_path: main_app.root_path, **options)
|
|
7
|
+
raise ArgumentError, "Must provide customer: parameter. Call user.set_payment_processor(:purchasekit) first." unless customer
|
|
8
|
+
|
|
9
|
+
builder = PurchaseKit::Pay::PaywallBuilder.new(self, customer)
|
|
10
|
+
|
|
11
|
+
form_data = (options.delete(:data) || {}).merge(
|
|
12
|
+
controller: "purchasekit-pay--paywall",
|
|
13
|
+
purchasekit_pay__paywall_customer_id_value: customer.id
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
form_with(url: purchasekit_pay.purchases_path, id: "purchasekit_paywall", data: form_data, **options) do |form|
|
|
17
|
+
hidden = hidden_field_tag(:customer_id, customer.id)
|
|
18
|
+
hidden += hidden_field_tag(:success_path, success_path)
|
|
19
|
+
hidden += hidden_field_tag(:environment, "sandbox", data: {purchasekit_pay__paywall_target: "environment"})
|
|
20
|
+
hidden + capture { yield builder }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class PurchaseKit::Pay::PaywallBuilder
|
|
26
|
+
def initialize(template, customer)
|
|
27
|
+
@template = template
|
|
28
|
+
@customer = customer
|
|
29
|
+
@current_product = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def plan_option(product:, selected: false, input_class: nil, **options, &block)
|
|
33
|
+
input_id = "purchasekit_plan_#{product.id.parameterize.underscore}"
|
|
34
|
+
|
|
35
|
+
radio = @template.radio_button_tag(
|
|
36
|
+
:product_id,
|
|
37
|
+
product.id,
|
|
38
|
+
selected,
|
|
39
|
+
id: input_id,
|
|
40
|
+
class: input_class,
|
|
41
|
+
autocomplete: "off",
|
|
42
|
+
data: {
|
|
43
|
+
purchasekit_pay__paywall_target: "planRadio",
|
|
44
|
+
apple_store_product_id: product.apple_product_id,
|
|
45
|
+
google_store_product_id: product.google_product_id
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@current_product = product
|
|
50
|
+
label = @template.label_tag(input_id, **options) { @template.capture(&block) }
|
|
51
|
+
@current_product = nil
|
|
52
|
+
|
|
53
|
+
radio + label
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def price(**options, &block)
|
|
57
|
+
raise "price must be called within a plan_option block" unless @current_product
|
|
58
|
+
|
|
59
|
+
data = (options.delete(:data) || {}).merge(
|
|
60
|
+
purchasekit_pay__paywall_target: "price",
|
|
61
|
+
apple_store_product_id: @current_product.apple_product_id,
|
|
62
|
+
google_store_product_id: @current_product.google_product_id
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
loading_content = block ? @template.capture(&block) : "Loading..."
|
|
66
|
+
@template.content_tag(:span, loading_content, data: data, **options)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def submit(text = "Subscribe", **options)
|
|
70
|
+
data = (options.delete(:data) || {}).merge(
|
|
71
|
+
purchasekit_pay__paywall_target: "submitButton",
|
|
72
|
+
turbo_submits_with: text
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@template.submit_tag(text, disabled: true, data: data, **options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def restore_link(text: "Restore purchases", **options)
|
|
79
|
+
data = (options.delete(:data) || {}).merge(
|
|
80
|
+
action: "click->purchasekit-pay--paywall#restore"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@template.link_to(text, "#", data: data, **options)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
|
|
2
|
+
|
|
3
|
+
export default class extends BridgeComponent {
|
|
4
|
+
static component = "paywall"
|
|
5
|
+
static targets = ["planRadio", "price", "submitButton", "response", "environment"]
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
super.connect()
|
|
9
|
+
this.#fetchPrices()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
responseTargetConnected(element) {
|
|
13
|
+
const correlationId = element.dataset.correlationId
|
|
14
|
+
const productIds = this.#productIds(element)
|
|
15
|
+
|
|
16
|
+
element.remove()
|
|
17
|
+
this.#disableForm()
|
|
18
|
+
this.#triggerNativePurchase(productIds, correlationId)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
restore(event) {
|
|
22
|
+
event.preventDefault()
|
|
23
|
+
this.send("restore")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#triggerNativePurchase(productIds, correlationId) {
|
|
27
|
+
this.send("purchase", { ...productIds, correlationId }, message => {
|
|
28
|
+
const { status, error } = message.data
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
console.error(error)
|
|
32
|
+
alert(`Purchase error: ${error}`)
|
|
33
|
+
this.#enableForm()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (status == "cancelled") {
|
|
37
|
+
this.#enableForm()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// On success, keep showing processing state.
|
|
41
|
+
// Turbo Stream will update the UI when webhook completes.
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#fetchPrices() {
|
|
46
|
+
const products = this.priceTargets.map(el => this.#productIds(el))
|
|
47
|
+
|
|
48
|
+
this.send("prices", { products }, message => {
|
|
49
|
+
const { prices, environment, error } = message.data
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
console.error(error)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (prices) {
|
|
57
|
+
this.#setPrices(prices)
|
|
58
|
+
this.#setEnvironment(environment)
|
|
59
|
+
this.#enableForm()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#setEnvironment(environment) {
|
|
65
|
+
if (this.hasEnvironmentTarget && environment) {
|
|
66
|
+
this.environmentTarget.value = environment
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#setPrices(prices) {
|
|
71
|
+
this.priceTargets.forEach(el => {
|
|
72
|
+
const { appleStoreProductId, googleStoreProductId } = this.#productIds(el)
|
|
73
|
+
const price = prices[appleStoreProductId] || prices[googleStoreProductId]
|
|
74
|
+
|
|
75
|
+
if (price) {
|
|
76
|
+
el.textContent = price
|
|
77
|
+
} else {
|
|
78
|
+
console.error(`No price found for product.`)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#productIds(element) {
|
|
84
|
+
return {
|
|
85
|
+
appleStoreProductId: element.dataset.appleStoreProductId,
|
|
86
|
+
googleStoreProductId: element.dataset.googleStoreProductId
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#enableForm() {
|
|
91
|
+
this.planRadioTargets.forEach(radio => radio.disabled = false)
|
|
92
|
+
if (this.hasSubmitButtonTarget) {
|
|
93
|
+
this.submitButtonTarget.disabled = false
|
|
94
|
+
if (this.#originalButtonText) {
|
|
95
|
+
this.submitButtonTarget.innerHTML = this.#originalButtonText
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#disableForm() {
|
|
101
|
+
this.planRadioTargets.forEach(radio => radio.disabled = true)
|
|
102
|
+
if (this.hasSubmitButtonTarget) {
|
|
103
|
+
this.#originalButtonText = this.submitButtonTarget.innerHTML
|
|
104
|
+
this.submitButtonTarget.disabled = true
|
|
105
|
+
const processingText = this.submitButtonTarget.dataset.processingText || "Processing..."
|
|
106
|
+
this.submitButtonTarget.innerHTML = processingText
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#originalButtonText = null
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Purchasekit
|
|
3
|
+
class Customer < Pay::Customer
|
|
4
|
+
# Use base class name for dom_id to keep stream names consistent
|
|
5
|
+
def self.model_name
|
|
6
|
+
Pay::Customer.model_name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def charge(amount, options = {})
|
|
10
|
+
raise Pay::Purchasekit::Error, "One-time charges not supported. Use in-app purchases."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
|
14
|
+
raise Pay::Purchasekit::Error, "Subscriptions must be initiated through the native app."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_payment_method(payment_method_id, default: false)
|
|
18
|
+
raise Pay::Purchasekit::Error, "Payment methods managed by App Store or Google Play."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Purchasekit
|
|
3
|
+
class Subscription < Pay::Subscription
|
|
4
|
+
def cancel(**options)
|
|
5
|
+
raise Pay::Purchasekit::Error, "Cancel through App Store or Google Play."
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def cancel_now!(**options)
|
|
9
|
+
raise Pay::Purchasekit::Error, "Cancel through App Store or Google Play."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resume
|
|
13
|
+
raise Pay::Purchasekit::Error, "Resume through App Store or Google Play."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def swap(plan, **options)
|
|
17
|
+
raise Pay::Purchasekit::Error, "Change plans through App Store or Google Play."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def change_quantity(quantity, **options)
|
|
21
|
+
raise Pay::Purchasekit::Error, "Quantity changes not supported for in-app purchases."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def paused?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pause(**options)
|
|
29
|
+
raise Pay::Purchasekit::Error, "Pause through App Store or Google Play."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= turbo_stream.append "purchasekit_paywall" do %>
|
|
2
|
+
<div data-purchasekit-pay--paywall-target="response"
|
|
3
|
+
data-correlation-id="<%= @intent.uuid %>"
|
|
4
|
+
data-apple-store-product-id="<%= @intent.product.apple_product_id %>"
|
|
5
|
+
data-google-store-product-id="<%= @intent.product.google_product_id %>">
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Pin Hotwire Native Bridge dependency
|
|
2
|
+
# Note: If your app already pins this package, your version will take precedence
|
|
3
|
+
# since the app's importmap.rb loads after the gem's importmap.rb
|
|
4
|
+
pin "@hotwired/hotwire-native-bridge", to: "@hotwired--hotwire-native-bridge.js" # @1.2.2
|
|
5
|
+
|
|
6
|
+
pin_all_from PurchaseKit::Pay::Engine.root.join("app/javascript/controllers"), under: "purchasekit-pay", to: "controllers"
|
|
7
|
+
|
|
8
|
+
pin "purchasekit-pay/turbo_actions", to: "purchasekit_pay/turbo_actions.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Purchasekit
|
|
3
|
+
class Error < Pay::Error
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def enabled?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def setup
|
|
13
|
+
# No setup required
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure_webhooks
|
|
17
|
+
Pay::Webhooks.configure do |events|
|
|
18
|
+
events.subscribe "purchasekit.subscription.created", PurchaseKit::Pay::Webhooks::SubscriptionCreated.new
|
|
19
|
+
events.subscribe "purchasekit.subscription.updated", PurchaseKit::Pay::Webhooks::SubscriptionUpdated.new
|
|
20
|
+
events.subscribe "purchasekit.subscription.canceled", PurchaseKit::Pay::Webhooks::SubscriptionCanceled.new
|
|
21
|
+
events.subscribe "purchasekit.subscription.expired", PurchaseKit::Pay::Webhooks::SubscriptionExpired.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "httparty"
|
|
2
|
+
|
|
3
|
+
module PurchaseKit
|
|
4
|
+
class ApiClient
|
|
5
|
+
def get(path)
|
|
6
|
+
request(:get, path)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def post(path, body = {})
|
|
10
|
+
request(:post, path, body)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def request(method, path, body = nil)
|
|
16
|
+
options = {headers: headers}
|
|
17
|
+
options[:body] = body.to_json if body
|
|
18
|
+
|
|
19
|
+
HTTParty.public_send(method, url(path), options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def url(path)
|
|
23
|
+
"#{config.api_url}/api/v1/apps/#{config.app_id}#{path}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def headers
|
|
27
|
+
{
|
|
28
|
+
"Authorization" => "Bearer #{config.api_key}",
|
|
29
|
+
"Accept" => "application/json",
|
|
30
|
+
"Content-Type" => "application/json"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def config
|
|
35
|
+
PurchaseKit::Pay.config
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :api_key, :api_url, :app_id, :webhook_secret
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@api_url = "https://purchasekit.dev"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :config
|
|
13
|
+
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(config)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace PurchaseKit::Pay
|
|
5
|
+
|
|
6
|
+
initializer "purchasekit_pay.register_processor", before: :load_config_initializers do
|
|
7
|
+
::Pay.enabled_processors << :purchasekit unless ::Pay.enabled_processors.include?(:purchasekit)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
initializer "purchasekit_pay.webhooks", after: :load_config_initializers do
|
|
11
|
+
::Pay::Purchasekit.configure_webhooks
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "purchasekit_pay.helpers" do
|
|
15
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
16
|
+
helper PurchaseKit::Pay::PaywallHelper
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "purchasekit_pay.importmap", before: "importmap" do |app|
|
|
21
|
+
if app.config.respond_to?(:importmap)
|
|
22
|
+
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
|
23
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
initializer "purchasekit_pay.assets" do |app|
|
|
28
|
+
if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:paths)
|
|
29
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
30
|
+
app.config.assets.precompile += %w[purchasekit-pay/manifest.js]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
class Webhook
|
|
4
|
+
def self.queue(event)
|
|
5
|
+
new(event).queue
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(event)
|
|
9
|
+
@event = event
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def queue
|
|
13
|
+
return unless listening?
|
|
14
|
+
|
|
15
|
+
record = ::Pay::Webhook.create!(processor: :purchasekit, event_type:, event: @event)
|
|
16
|
+
ProcessWebhookJob.perform_later(record.id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def event_type
|
|
22
|
+
@event[:type]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def listening?
|
|
26
|
+
::Pay::Webhooks.delegator.listening?("purchasekit.#{event_type}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ProcessWebhookJob < ::ActiveJob::Base
|
|
30
|
+
def perform(webhook_id)
|
|
31
|
+
::Pay::Webhook.find(webhook_id).process!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
module Webhooks
|
|
4
|
+
class Base
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def update_subscription(event, attributes)
|
|
8
|
+
pay_subscription = find_subscription(event)
|
|
9
|
+
return unless pay_subscription
|
|
10
|
+
|
|
11
|
+
pay_subscription.update!(attributes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find_subscription(event)
|
|
15
|
+
::Pay::Subscription.find_by(processor_id: event["subscription_id"])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse_time(value)
|
|
19
|
+
return nil if value.blank?
|
|
20
|
+
Time.zone.parse(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
module Webhooks
|
|
4
|
+
class SubscriptionCreated < Base
|
|
5
|
+
include ActionView::RecordIdentifier
|
|
6
|
+
include Turbo::Streams::ActionHelper
|
|
7
|
+
|
|
8
|
+
def call(event)
|
|
9
|
+
customer = ::Pay::Customer.find(event["customer_id"])
|
|
10
|
+
|
|
11
|
+
subscription = ::Pay::Purchasekit::Subscription.find_or_initialize_by(
|
|
12
|
+
customer: customer,
|
|
13
|
+
processor_id: event["subscription_id"]
|
|
14
|
+
)
|
|
15
|
+
is_new = subscription.new_record?
|
|
16
|
+
|
|
17
|
+
subscription.update!(
|
|
18
|
+
name: event["subscription_name"] || ::Pay.default_product_name,
|
|
19
|
+
processor_plan: event["store_product_id"],
|
|
20
|
+
status: :active,
|
|
21
|
+
quantity: 1,
|
|
22
|
+
current_period_start: parse_time(event["current_period_start"]),
|
|
23
|
+
current_period_end: parse_time(event["current_period_end"]),
|
|
24
|
+
ends_at: parse_time(event["ends_at"]),
|
|
25
|
+
data: (subscription.data || {}).merge("store" => event["store"])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return unless is_new
|
|
29
|
+
|
|
30
|
+
redirect_path = event["success_path"] || Rails.application.routes.url_helpers.root_path
|
|
31
|
+
Turbo::StreamsChannel.broadcast_stream_to(
|
|
32
|
+
dom_id(customer),
|
|
33
|
+
content: turbo_stream_action_tag(:redirect, url: redirect_path)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
module Webhooks
|
|
4
|
+
class SubscriptionUpdated < Base
|
|
5
|
+
include ActionView::RecordIdentifier
|
|
6
|
+
include Turbo::Streams::ActionHelper
|
|
7
|
+
|
|
8
|
+
def call(event)
|
|
9
|
+
update_subscription(event,
|
|
10
|
+
processor_plan: event["store_product_id"],
|
|
11
|
+
status: event["status"],
|
|
12
|
+
current_period_start: parse_time(event["current_period_start"]),
|
|
13
|
+
current_period_end: parse_time(event["current_period_end"]),
|
|
14
|
+
ends_at: parse_time(event["ends_at"])
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
broadcast_redirect(event) if event["success_path"].present?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def broadcast_redirect(event)
|
|
23
|
+
customer = ::Pay::Customer.find(event["customer_id"])
|
|
24
|
+
Turbo::StreamsChannel.broadcast_stream_to(
|
|
25
|
+
dom_id(customer),
|
|
26
|
+
content: turbo_stream_action_tag(:redirect, url: event["success_path"])
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Pay
|
|
3
|
+
module Webhooks
|
|
4
|
+
autoload :Base, "purchasekit/pay/webhooks/base"
|
|
5
|
+
autoload :SubscriptionCreated, "purchasekit/pay/webhooks/subscription_created"
|
|
6
|
+
autoload :SubscriptionUpdated, "purchasekit/pay/webhooks/subscription_updated"
|
|
7
|
+
autoload :SubscriptionCanceled, "purchasekit/pay/webhooks/subscription_canceled"
|
|
8
|
+
autoload :SubscriptionExpired, "purchasekit/pay/webhooks/subscription_expired"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
class Product
|
|
3
|
+
attr_reader :id, :apple_product_id, :google_product_id
|
|
4
|
+
|
|
5
|
+
def initialize(id:, apple_product_id: nil, google_product_id: nil)
|
|
6
|
+
@id = id
|
|
7
|
+
@apple_product_id = apple_product_id
|
|
8
|
+
@google_product_id = google_product_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def store_product_id(platform:)
|
|
12
|
+
case platform
|
|
13
|
+
when :apple then apple_product_id
|
|
14
|
+
when :google then google_product_id
|
|
15
|
+
else raise ArgumentError, "Unknown platform: #{platform}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def find(id)
|
|
21
|
+
response = ApiClient.new.get("/products/#{id}")
|
|
22
|
+
|
|
23
|
+
case response.code
|
|
24
|
+
when 200
|
|
25
|
+
new(
|
|
26
|
+
id: response["id"],
|
|
27
|
+
apple_product_id: response["apple_product_id"],
|
|
28
|
+
google_product_id: response["google_product_id"]
|
|
29
|
+
)
|
|
30
|
+
when 404
|
|
31
|
+
raise PurchaseKit::Pay::NotFoundError, "Product not found: #{id}"
|
|
32
|
+
else
|
|
33
|
+
raise PurchaseKit::Pay::Error, "API error: #{response.code} #{response.message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module PurchaseKit
|
|
2
|
+
module Purchase
|
|
3
|
+
class Intent
|
|
4
|
+
attr_reader :id, :uuid, :product
|
|
5
|
+
|
|
6
|
+
def initialize(id:, uuid:, product:)
|
|
7
|
+
@id = id
|
|
8
|
+
@uuid = uuid
|
|
9
|
+
@product = product
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def create(product_id:, customer_id:, success_path: nil, environment: nil)
|
|
14
|
+
response = ApiClient.new.post("/purchase/intents", {
|
|
15
|
+
product_id: product_id,
|
|
16
|
+
customer_id: customer_id,
|
|
17
|
+
success_path: success_path,
|
|
18
|
+
environment: environment
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
case response.code
|
|
22
|
+
when 201
|
|
23
|
+
product_data = response["product"]
|
|
24
|
+
product = Product.new(
|
|
25
|
+
id: product_data["id"],
|
|
26
|
+
apple_product_id: product_data["apple_product_id"],
|
|
27
|
+
google_product_id: product_data["google_product_id"]
|
|
28
|
+
)
|
|
29
|
+
new(id: response["id"], uuid: response["uuid"], product: product)
|
|
30
|
+
when 402
|
|
31
|
+
raise PurchaseKit::Pay::SubscriptionRequiredError, response["error"] || "Subscription required for production purchases"
|
|
32
|
+
when 404
|
|
33
|
+
raise PurchaseKit::Pay::NotFoundError, "App or product not found"
|
|
34
|
+
else
|
|
35
|
+
raise PurchaseKit::Pay::Error, "API error: #{response.code} #{response.message}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "pay"
|
|
2
|
+
require "purchasekit/pay/version"
|
|
3
|
+
require "purchasekit/pay/configuration"
|
|
4
|
+
require "purchasekit/pay/error"
|
|
5
|
+
require "purchasekit/pay/engine"
|
|
6
|
+
require "purchasekit/pay/webhooks"
|
|
7
|
+
require "purchasekit/pay/webhook"
|
|
8
|
+
require "purchasekit/api_client"
|
|
9
|
+
require "purchasekit/product"
|
|
10
|
+
require "purchasekit/purchase/intent"
|
|
11
|
+
require "pay/purchasekit"
|
metadata
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: purchasekit-pay
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Joe Masilotti
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: httparty
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.22'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.22'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: pay
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '11.4'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '11.4'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rails
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '7.0'
|
|
48
|
+
- - "<"
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '9'
|
|
51
|
+
type: :runtime
|
|
52
|
+
prerelease: false
|
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '7.0'
|
|
58
|
+
- - "<"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '9'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: vcr
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '6.0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: webmock
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
description: Add mobile in-app purchases to your Rails app with PurchaseKit and Pay.
|
|
90
|
+
email:
|
|
91
|
+
- joe@masilotti.com
|
|
92
|
+
executables: []
|
|
93
|
+
extensions: []
|
|
94
|
+
extra_rdoc_files: []
|
|
95
|
+
files:
|
|
96
|
+
- MIT-LICENSE
|
|
97
|
+
- README.md
|
|
98
|
+
- Rakefile
|
|
99
|
+
- app/controllers/purchase_kit/pay/application_controller.rb
|
|
100
|
+
- app/controllers/purchase_kit/pay/purchases_controller.rb
|
|
101
|
+
- app/controllers/purchase_kit/pay/webhooks_controller.rb
|
|
102
|
+
- app/helpers/purchase_kit/pay/paywall_helper.rb
|
|
103
|
+
- app/javascript/controllers/purchasekit_pay/paywall_controller.js
|
|
104
|
+
- app/javascript/purchasekit_pay/turbo_actions.js
|
|
105
|
+
- app/models/pay/purchasekit/charge.rb
|
|
106
|
+
- app/models/pay/purchasekit/customer.rb
|
|
107
|
+
- app/models/pay/purchasekit/subscription.rb
|
|
108
|
+
- app/views/purchase_kit/pay/purchases/_subscription_required.html.erb
|
|
109
|
+
- app/views/purchase_kit/pay/purchases/create.turbo_stream.erb
|
|
110
|
+
- config/importmap.rb
|
|
111
|
+
- config/routes.rb
|
|
112
|
+
- lib/pay/purchasekit.rb
|
|
113
|
+
- lib/purchasekit-pay.rb
|
|
114
|
+
- lib/purchasekit/api_client.rb
|
|
115
|
+
- lib/purchasekit/pay/configuration.rb
|
|
116
|
+
- lib/purchasekit/pay/engine.rb
|
|
117
|
+
- lib/purchasekit/pay/error.rb
|
|
118
|
+
- lib/purchasekit/pay/version.rb
|
|
119
|
+
- lib/purchasekit/pay/webhook.rb
|
|
120
|
+
- lib/purchasekit/pay/webhooks.rb
|
|
121
|
+
- lib/purchasekit/pay/webhooks/base.rb
|
|
122
|
+
- lib/purchasekit/pay/webhooks/subscription_canceled.rb
|
|
123
|
+
- lib/purchasekit/pay/webhooks/subscription_created.rb
|
|
124
|
+
- lib/purchasekit/pay/webhooks/subscription_expired.rb
|
|
125
|
+
- lib/purchasekit/pay/webhooks/subscription_updated.rb
|
|
126
|
+
- lib/purchasekit/product.rb
|
|
127
|
+
- lib/purchasekit/purchase/intent.rb
|
|
128
|
+
homepage: https://purchasekit.dev
|
|
129
|
+
licenses:
|
|
130
|
+
- MIT
|
|
131
|
+
metadata:
|
|
132
|
+
homepage_uri: https://purchasekit.dev
|
|
133
|
+
source_code_uri: https://github.com/purchasekit/purchasekit-pay
|
|
134
|
+
post_install_message:
|
|
135
|
+
rdoc_options: []
|
|
136
|
+
require_paths:
|
|
137
|
+
- lib
|
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '3.1'
|
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '0'
|
|
148
|
+
requirements: []
|
|
149
|
+
rubygems_version: 3.4.10
|
|
150
|
+
signing_key:
|
|
151
|
+
specification_version: 4
|
|
152
|
+
summary: PurchaseKit payment processor for Pay
|
|
153
|
+
test_files: []
|